How ASP.NET Core Dependency Injection Works

ASP.NET Core includes a native, lightweight Inversion of Control (IoC) container that provides Dependency Injection (DI) out of the box. This article explains how the built-in DI system operates, focusing on service registration, the three fundamental service lifetimes, constructor injection, and how the runtime automatically manages object disposal without requiring third-party libraries.

The Built-in IoC Container

At the core of ASP.NET Core's dependency injection are two primary components: IServiceCollection and IServiceProvider.

During application startup—typically in Program.cs—you register your application's dependencies into the builder.Services collection, which implements IServiceCollection. When the application builds, this collection compiles into an IServiceProvider, which acts as the runtime container responsible for instantiating types and resolving their dependencies whenever they are requested.

Service Lifetimes

When registering a dependency, you must specify its lifetime. The built-in container supports three lifetimes:

  1. Transient (AddTransient)
    Services are created each time they are requested from the service container. This lifetime works best for lightweight, stateless services.

  2. Scoped (AddScoped)
    Services are created once per client request (connection). In a web application, a scoped service is shared across all components processing the same HTTP request, and it is disposed of when the request finishes.

  3. Singleton (AddSingleton)
    Services are created the first time they are requested (or when provided as an instance at startup) and remain alive for the lifetime of the application. Every subsequent request receives the exact same instance.

Registering Services

To register services, map an abstraction (an interface) to a concrete implementation in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register services with their respective lifetimes
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

var app = builder.Build();

Constructor Injection

ASP.NET Core primarily uses constructor injection to supply dependencies to classes, such as controllers, minimal API endpoints, or other services.

When a class requires a dependency, you declare it as a parameter in its constructor. The runtime inspects the constructor parameters via reflection, queries the IServiceProvider for matching types, creates the necessary instances, and passes them in.

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IEmailSender _emailSender;

    public OrderService(IOrderRepository repository, IEmailSender emailSender)
    {
        _repository = repository;
        _emailSender = emailSender;
    }

    public void CompleteOrder(int orderId)
    {
        _repository.MarkAsComplete(orderId);
        _emailSender.Send("Order completed.");
    }
}

If OrderService itself is registered in the DI container, requesting it will cause the container to recursively resolve both IOrderRepository and IEmailSender first.

Automatic Disposal and Cleanup

The container automatically manages the lifecycle of the objects it creates. If a service implements IDisposable or IAsyncDisposable, the container will call Dispose or DisposeAsync when the service's lifetime ends:

  • Transient and Scoped instances are disposed of at the end of the HTTP request scope.
  • Singleton instances are disposed of when the application shuts down.