Dependency Injection

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

ASP.NET Core is built on dependency injection: the framework, and your code, declare the services they need as constructor parameters and the container supplies them.

The container

builder.Services is an IServiceCollection — a list of service descriptors (service type, implementation, lifetime). builder.Build() compiles it into an IServiceProvider that resolves instances.

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddHttpClient<IPaymentGateway, StripeGateway>();
builder.Services.AddSingleton(TimeProvider.System);

Lifetimes

Lifetime One instance per…​

AddSingleton

Application. Created once, shared by every request. Must be thread-safe.

AddScoped

Scope — in ASP.NET Core, one HTTP request. The typical choice for services that touch a DbContext.

AddTransient

Resolution. A new instance every time it is injected.

Captive dependency: if a singleton takes a scoped (or transient) service in its constructor, that short-lived service is trapped for the whole application lifetime. In Development the container validates scopes at build time and throws when this happens.

// WRONG: singleton capturing a scoped DbContext
builder.Services.AddSingleton<CacheWarmer>();       // ctor: CacheWarmer(AppDbContext db)

// RIGHT: take IServiceScopeFactory and open a scope per unit of work
public sealed class CacheWarmer(IServiceScopeFactory scopeFactory)
{
    public async Task WarmAsync()
    {
        using var scope = scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // ...
    }
}

Registration techniques

services.AddScoped<IClock, SystemClock>();                       // implementation type
services.AddSingleton<IClock>(new SystemClock());                // instance
services.AddScoped<IClock>(sp => new SystemClock(sp.GetRequiredService<TimeProvider>())); // factory

services.TryAddScoped<IClock, SystemClock>();                    // no-op if already registered
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, MyFilter>()); // add once to a set

// keyed services -- pick an implementation by key
services.AddKeyedScoped<INotifier, EmailNotifier>("email");
services.AddKeyedScoped<INotifier, SmsNotifier>("sms");
public sealed class SignupHandler([FromKeyedServices("email")] INotifier notifier);

Consuming services

  • Controllers, Razor Pages, SignalR hubs, IMiddleware — constructor injection.

  • Minimal API handlers — add the service as a parameter; the framework infers it from the container. [FromKeyedServices("…​")] selects a keyed one.

    app.MapPost("/orders", async (CreateOrder cmd, IOrderService orders) => await orders.CreateAsync(cmd));
  • Razor / Blazor components — @inject IOrderService Orders (or [Inject] on a property).

    @inject IClock Clock
    <p>Server time: @Clock.UtcNow</p>
  • Singletons / background services — inject IServiceScopeFactory and open a scope per unit of work.

Replacing the container

Third-party containers plug in via UseServiceProviderFactory. Autofac, for example, adds assembly scanning and richer registration:

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(b =>
    b.RegisterAssemblyTypes(typeof(Program).Assembly)
     .Where(t => t.Name.EndsWith("Repository"))
     .AsImplementedInterfaces());

The built-in container is deliberately minimal; reach for a replacement only when you need features it lacks. See DI guidelines.

Lifetimes across two requests

Singleton, scoped, and transient resolution across two consecutive HTTP requests

A singleton is the same instance in both requests; a scoped service is one instance per request (shared by every consumer within that request); a transient service is a fresh instance at every injection point.