.NET Core  

Understanding Dependency Injection (DI) in .NET Core with Simple Examples

Introduction

When building modern applications, it’s important to write code that’s easy to manage, flexible, and simple to test. That’s exactly what Dependency Injection (DI) helps us do. Instead of a class creating everything it needs by itself, DI allows those dependencies to be provided from the outside, making the code cleaner and more maintainable.

Dependency-injection

In this article, we will learn:

  • What is Dependency Injection?

  • Why do we need it?

  • A simple example (Car and Engine)

  • A real-world example ( ILogger<T> in .NET Core)

  • What Happens Behind the Scenes with ILogger<T>

  • Types of DI and Service Lifetimes

  • Benefits of using DI

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where a class receives its dependencies from the outside , instead of creating them inside.

👉 In simple words:

  • A Car needs an Engine

  • Instead of the car creating the Engine, we inject the Engine from outside.

This makes code flexible, maintainable, and testable .

Without DI (Tight Coupling):


    public class Engine
{
    public string Start() => "Engine started.";
}

public class Car
{
    private Engine _engine = new Engine(); // ❌ Car is tightly bound to Engine

    public string Run()
    {
        return _engine.Start() + " Car is running.";
    }
}

⚠️ Problem: If we want to use a PetrolEngine or ElectricCar, we must modify the Car class. This breaks the Open/Closed Principle (OCP) of SOLID.

With Dependency Injection (Loose Coupling)

Step 1. Create an Interface

public interface IEngine
{
    string Start();
}

Step 2. Implement Engines

public class PetrolEngine : IEngine
{
    public string Start() => "Petrol engine started.";
}

public class ElectricEngine : IEngine
{
    public string Start() => "Electric engine started.";
}

Step 3. Inject Engine into Car

public class Car
{
    private readonly IEngine _engine;

    // Constructor Injection
    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public string Run()
    {
        return _engine.Start() + " Car is running.";
    }
}

Step 4. Register in Program.cs

var builder = WebApplication.CreateBuilder(args);

// Registering the DI services
builder.Services.AddScoped<IEngine, ElectricEngine>();
builder.Services.AddScoped<Car>();

var app = builder.Build();

app.MapGet("/run", (Car car) => car.Run());

app.Run();

👉 Now, if you hit /run, you will see:
Electric engine started. Car is running.

👉 If tomorrow you want a petrol engine, just change the registration line:

// ElectricEngine can be replaced with PetrolEngine easily. 
builder.Services.AddScoped<IEngine, PetrolEngine>();

Real-World Example: ILogger<T> in .NET Core

One of the best practical uses of DI is logging.

ILogger<T> in .NET Core is a built-in service that helps you write logs for your application, like messages, warnings, or errors. You don’t have to create it yourself—.NET Core provides it automatically using dependency injection. The <T> tells the logger which class it’s coming from, so when you look at the logs, you can easily see where each message originated. It’s a simple and clean way to keep track of what’s happening in your app while keeping your code organized.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger) // Constructor Injection: ILogger is passed in automatically
    
    {
        _logger = logger;
    }

    [HttpGet("welcome")]
    public string GetWelcome()
    {
         // Using the injected logger 
        _logger.LogInformation("Welcome Rudra! endpoint was called!");
        return "Hello, Dependency Injection in .NET Core!";
    }
}

Here, ILogger<HomeController> is automatically injected by the DI container.
No need to create a logger manually i.e. .NET handles it for us.

When you call /home/welcome, you’ll see a log like this in the console:

info: MyApp.Controllers.HomeController[0]
       Welcome endpoint was called!

👉Important: What Happens Behind the Scenes with ILogger<T>

When you access the URL /home/welcome in your browser, ASP .NET Core creates a new instance of the HomeController. It notices that the controller’s constructor requires an ILogger<HomeController>. At this point, it asks the Dependency Injection (DI) container:

“Do we have a service for ILogger<HomeController>?”

The DI container already has it registered internally, so it automatically provides an instance of ILogger<HomeController> to the controller. When the GetWelcome() method is called, the log message is sent to the console (or whichever logging provider is configured).

By default, in .NET 6/7/8 Web API, you’ll see something like this in your console:

info: MyApp.Controllers.HomeController[0]
      Welcome endpoint was called!

This output comes directly from the code:

_logger.LogInformation("Welcome endpoint was called!");

The key point here is that you never manually create the logger and you don’t have to configure how the logs are written—the DI container does it for you. Instead of the class being responsible for creating its own dependencies, it simply asks for them in the constructor. This is exactly what Dependency Injection is all about: your class declares what it needs, and the framework provides it automatically.

Types of Dependency Injection

1. Constructor Injection (Most Common)

We already followed constructor injection in example above. In this approach, dependencies are passed into a class through its constructor. It ensures that the dependency is available as soon as the object is created. This is the most widely used method in .NET Core and is also considered best practice.

2. Method Injection

Method Injection is when a class receives a dependency only when a specific method is called, instead of getting it through the constructor or a property. In other words, the dependency is passed as a parameter to the method that needs it. This is useful when the dependency is needed only occasionally or for certain actions. However, if multiple methods need the same dependency, you may end up passing it repeatedly, so it’s less commonly used than constructor injection.

3. Property Injection

Property Injection allows you to provide a class with its dependency through a public property instead of the constructor. In other words, you first create the object and then assign the dependency to its property. This can be useful when the dependency is optional or only needed later. However, you need to be careful—if the property is not set before using it, it can cause errors, which is why this approach is used less often than constructor injection.

Service Lifetimes in .NET Core

Service lifetimes in .NET Core determine how long a dependency object lives when it’s provided by the DI container. There are three main types:

  • Singleton: A single instance is created and shared across the entire application. For example, a configuration service that never changes can be registered as singleton.

  • Scoped: A new instance is created for each HTTP request. For example, a user session service can be scoped so each request gets its own instance.

  • Transient: A new instance is created every time the dependency is requested. For example, a lightweight helper service that performs simple calculations can be transient.

Choosing the right lifetime ensures efficient resource use, predictable behavior, and cleaner code while using dependency injection effectively.

builder.Services.AddSingleton<IEngine, PetrolEngine>();  // same instance for whole app
builder.Services.AddScoped<IEngine, PetrolEngine>();     // one per request
builder.Services.AddTransient<IEngine, PetrolEngine>();  // new every time

Benefits of Dependency Injection

  • Makes code cleaner and easier to maintain.

  • Reduces tight coupling between classes.

  • Allows you to swap implementations without changing your code.

  • Makes unit testing easier by enabling mock dependencies.

  • Leads to organized, reusable, and testable code.

  • Helps build scalable and flexible applications, especially in large projects.

Conclusion

Dependency Injection (DI) is a powerful design pattern in .NET Core that helps make your applications cleaner, more maintainable, and easier to test. By letting the framework provide the dependencies, you reduce tight coupling, improve flexibility, and can easily swap implementations without changing your classes. We saw how DI works with a simple Car and Engine example and also in a real-world scenario using ILogger<T>, along with the different service lifetimes—Singleton, Scoped, and Transient. Understanding and using DI effectively is a key step toward writing professional, robust, and scalable .NET Core applications.

What’s Next?

In the next article, we will dive into Microservices in .NET Core. You’ll learn how to design and implement small, independent services that work together to form a larger application. We’ll also go through a practical example to see how microservices communicate, how to manage dependencies, and how DI plays a role in building scalable and maintainable applications.