Dependency Injection (DI) is at the heart of ASP.NET Core. You see it when you create controllers, register services, build middleware, or configure logging and configuration. Almost everything in ASP.NET Core is wired together using DI.

Yet many developers still only know it as:

“We add services in builder.Services and use them in constructor.”

This article takes you beyond that.

We’ll start from the absolute basics and gradually move into real-world, interview-level, and advanced concepts—all in a clear, structured, and practical way.

1. What is Dependency Injection?

In simple terms:

Dependency Injection is a technique where a class receives the objects it needs (its dependencies) from the outside, instead of creating them itself.

A dependency is just another class or service that your class needs to do its work.

Example without DI (Tight Coupling)

public class EmailService
{
    public void Send(string to, string message)
    {
        // send email logic
    }
}

public class Notification
{
    private readonly EmailService email = new EmailService(); //  Bad

    public void Notify(string userEmail)
    {
        email.Send(userEmail, "Welcome!");
    }
}

Here, Notification:

Example with DI (Loose Coupling)

public class EmailService
{
    public void Send(string to, string message)
    {
        // send email logic
    }
}

public class Notification
{
    private readonly EmailService email;

    public Notification(EmailService email) // Injected
    {
        email = email;
    }

    public void Notify(string userEmail)
    {
        email.Send(userEmail, "Welcome!");
    }
}

Now:

2. What Problem Does DI Solve?

Dependency Injection primarily solves the problem of tight coupling.

When a class creates its own dependencies:

Tightly coupled classes are hard to test, hard to replace, and hard to extend.

With DI, instead:

3. DI and the Dependency Inversion Principle (SOLID)

DI is closely connected to the Dependency Inversion Principle (DIP)—the “D” in SOLID:

High-level modules should not depend on low-level modules. Both should depend on abstractions

Example with Interface Abstraction

public interface IMessageService
{
    void Send(string msg);
}

public class EmailService : IMessageService
{
    public void Send(string msg)
    {
        // send email
    }
}

public class SmsService : IMessageService
{
    public void Send(string msg)
    {
        // send sms
    }
}

public class Notification
{
    private readonly IMessageService messageService;

    public Notification(IMessageService messageService)
    {
        messageService = messageService;
    }

    public void Notify(string message)
    {
        messageService.Send(message);
    }
}

Here:

4. Benefits of Dependency Injection

  1. Cleaner Code

    • Classes have single responsibilities.

    • No “new” scattered everywhere, creating dependencies.

  2. Easier Unit Testing

    • You can pass mocks or fakes into constructors.

    • No need to hit real databases, APIs, or external systems.

  3. Improved Maintainability

    • Changes in one service don’t force code changes everywhere.

    • Centralized configuration of dependencies.

  4. Flexibility and Extensibility

    • Swap implementations (e.g., Email provider) by changing DI registration.

  5. Encourages Good Architecture

    • Naturally pushes you toward SOLID, layered architecture, and clean code.

5. Types of Dependency Injection

Three main types of DI used in .NET:

  1. Constructor Injection (most common)

  2. Property Injection

  3. Method Injection

5.1 Constructor Injection (Preferred)

Constructor injection is where dependencies are passed via the constructor.

public class PaymentService
{
    private readonly ILogger _logger;

    public PaymentService(ILogger logger)
    {
        _logger = logger;
    }

    public void Process()
    {
        _logger.Log("Payment processed.");
    }
}

Why it’s preferred:

5.2 Property Injection

Dependencies are assigned via public properties (less common in ASP.NET Core, but still useful in some scenarios).

public class ReportService
{
    public ILogger Logger { get; set; }

    public void Generate()
    {
        Logger?.Log("Report generated.");
    }
}

Drawbacks

5.3 Method Injection

Dependencies are passed directly into the method that needs them.

Example using [FromServices] to inject a service directly into an action method:

public IActionResult Run([FromServices] IReportService report)
{
    return Ok(report.Generate());
}

This is useful when:

6. What is an IoC Container?

An IoC (Inversion of Control) Container is the mechanism that:

The IoC container is built in, so you don’t need third-party libraries to start.

Examples of IoC containers (in general):

In ASP.NET Core, we typically use the built-in container unless we have very advanced needs.

7. Registering Services in ASP.NET Core

In ASP.NET Core, DI registration is done in Program.cs.

builder.Services.AddTransient<IMessageService, EmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ILogService, LogService>();

This tells the IoC container:

Services are then resolved by:

8. Service Lifetimes: Transient, Scoped, Singleton

Service lifetime defines how long the service instance lives.

The list three lifetimes:

  1. Transient – new instance every time

  2. Scoped – one instance per HTTP request

  3. Singleton – one instance for the entire application

Let’s look at each in detail.

8.1 Transient

Definition: A new instance is created every time it’s requested.

builder.Services.AddTransient<IValidator, UserValidator>();

Use Transient for:

Pros

Cons

8.2 Scoped

Definition: One instance is created per HTTP request.

builder.Services.AddScoped<IProductService, ProductService>();

Use Scoped for:

Pros:

8.3 Singleton

Definition: One instance is created for the entire application lifetime.

builder.Services.AddSingleton<ILogService, LogService>();

Use Singleton for:

Pros

Cons

9. The Captive Dependency Problem

One of the more advanced topics is the captive dependency problem.

This happens when a long-living service (like a Singleton) depends on a shorter-living service (like Scoped).

For example:

builder.Services.AddSingleton<ReportingService>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();
public class ReportingService
{
    private readonly IUnitOfWork _uow;

    public ReportingService(IUnitOfWork uow)
    {
        _uow = uow; //  Scoped into Singleton
    }
}

Here:

“Cannot consume scoped service ‘X’ from singleton ‘Y’.”

This is a captive dependency and is considered a design smell.

Rule of thumb:

10. DI in Controllers (Typical Usage)

The classic controller example:

public class ProductsController : ControllerBase
{
    private readonly IProductService _service;

    public ProductsController(IProductService service) // usually Scoped
    {
        _service = service;
    }

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var product = _service.GetById(id);
        return Ok(product);
    }
}

Key points

11. DI in Middleware

Middleware supports constructor injection, but scoped services should be injected via InvokeAsync.

Constructor Injection for Singleton-like Services

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogService _log;

    public LoggingMiddleware(RequestDelegate next, ILogService log)
    {
        _next = next;
        _log = log;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _log.Log("Request started");
        await _next(context);
        _log.Log("Request finished");
    }
}

Here, ILogService It is likely Singleton and safe to inject in the constructor.

Method Injection for Scoped Services

For Scoped services, the suggestion is to use method parameters:

public async Task InvokeAsync(HttpContext context, IMyScopedService service)
{
    // This works - service is resolved per request
    await service.DoWorkAsync();
    await _next(context);
}

ASP.NET Core resolves the scoped service per request and passes it to InvokeAsync.

12. Method Injection in Controllers

The [FromServices] An attribute allows you to inject dependencies directly into individual action methods.

[HttpGet("report")]
public IActionResult GetReport([FromServices] IReportService reportService)
{
    var report = reportService.Generate();
    return Ok(report);
}

Use this pattern when:

13. Multiple Implementations of the Same Interface

As your application grows, you might need multiple implementations for the same interface (e.g., Email, SMS, Push notification). You can register multiple implementations and resolve them appropriately.

A common approach in ASP.NET Core is:

Example: Multiple IMessageService Implementations

public class SmsService : IMessageService
{
    public void Send(string msg) { /* send SMS */ }
}

public class EmailService : IMessageService
{
    public void Send(string msg) { /* send Email */ }
}

builder.Services.AddTransient<IMessageService, SmsService>();
builder.Services.AddTransient<IMessageService, EmailService>();
public class NotificationCenter
{
    private readonly IEnumerable<IMessageService> _messageServices;

    public NotificationCenter(IEnumerable<IMessageService> messageServices)
    {
        _messageServices = messageServices;
    }

    public void SendAll(string message)
    {
        foreach (var svc in _messageServices)
        {
            svc.Send(message);
        }
    }
}

Alternatively, you can create a factory that chooses by type (e.g., "sms", "email") using a dictionary strategy.

14. Best Practices for DI in ASP.NET Core

  1. Depend on Interfaces, not Concrete Types

    • Improves testability and flexibility.

  2. Use Constructor Injection for Required Dependencies

    • Keep constructors small and meaningful.

    • If more than 5–6 dependencies, consider refactoring that class.

  3. Avoid the Service Locator Pattern

    • Don’t inject IServiceProvider and manually resolve dependencies everywhere.

    • Let DI inject what you need.

  4. Choose Correct Lifetimes

    • DbContext → Scoped

    • Loggers → Singleton

    • Validation Services → Transient

  5. Avoid Captive Dependency

    • Don’t allow Singleton to depend on Scoped.

  6. Keep Services Stateless Where Possible

    • Easier to reason about and share (Singleton/Transient).

  7. Register Services in a Single Place

    • For maintainability, use extension methods for complex modules:

      public static class ServiceCollectionExtensions
      {
          public static IServiceCollection AddBilling(this IServiceCollection services)
          {
              services.AddScoped<IBillingService, BillingService>();
              // other billing registrations
              return services;
          }
      }

15. Common Mistakes with DI

16. Interview-Focused Recap

If you’re preparing for interviews, here are some typical questions from this article:

If you can confidently answer these, you have a strong grasp of DI in ASP.NET Core.