October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

Dependency Injection in ASP.NET Core: Registration, Lifetimes, and Scopes (.NET 10)

Dependency injection in ASP.NET Core registers services in Program.cs and supplies them to classes that declare them. This guide covers registration forms, the three lifetimes, scope boundaries, the captive dependency error, middleware, and keyed services.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Dependency injection in ASP.NET Core means you register your application’s services with the service collection, usually in Program.cs, and the framework supplies those services to any class that declares them, most often as constructor parameters. Each registration also picks a lifetime (transient, scoped, or singleton) that controls how often an instance is created and how long it lives. Most DI bugs in ASP.NET Core come from choosing the wrong lifetime, not from registration syntax.

This guide follows the current Microsoft Learn article, which targets .NET 10. The core registration model and lifetimes have been stable across recent ASP.NET Core releases, but features such as keyed services depend on the target framework, so readers on earlier targets should check the matching versioned documentation.

As an Amazon Associate I earn from qualifying purchases.

How registration works

Services are added to builder.Services before builder.Build() runs. A minimal example looks like this:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
builder.Services.AddSingleton<IClock, SystemClock>();

var app = builder.Build();

The overloads cover four common shapes. The Microsoft Learn overview of dependency injection in ASP.NET Core and the service registration guidance document the overload behavior in detail.

Registration form Example Use it when
Abstraction bound to implementation AddScoped<IOrderService, OrderService>() Consumers should depend on an interface, which is the usual choice for testable code.
Concrete type AddScoped<OrderService>() There is no interface worth defining, or you want the type itself to be the key.
Factory AddScoped<IOrderService>(sp => new OrderService(sp.GetRequiredService<IOrderRepository>())) Construction needs extra setup, configuration values, or conditional logic.
Existing instance AddSingleton<IClock>(new SystemClock()) You already hold a fully built object and want every consumer to receive it.

Choosing a lifetime

The lifetime answers one question: how long may an instance be reused? The table compares the three lifetimes on the axes that matter in web applications.

Lifetime Instance reuse boundary Typical use Main caution
Transient A new instance every time the service is resolved Lightweight, stateless helpers that should not share state Disposable transients need care; see the disposal section below
Scoped One instance per scope. In MVC and Razor Pages, one scope per HTTP request Per-request work such as unit-of-work objects and request-specific state Must never be captured by a singleton
Singleton One instance for the lifetime of the service provider, which is effectively the application Caches, configuration wrappers, clients designed for shared use Must be thread-safe, because concurrent requests share it

Transient

A transient service is created each time something asks for it. If an OrderController and a ReceiptBuilder both need an IEmailFormatter registered as transient, each receives a separate object. This suits services that hold per-call working state and are cheap to construct.

Scoped

A scoped service is created once per scope and reused for everything inside that scope. In a typical MVC or Razor Pages app, the framework opens one scope per HTTP request, so an IOrderService resolved by a controller and by a filter in the same request is the same object. The request ends, the scope is disposed, and the next request gets a fresh instance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Entity Framework Core’s AddDbContext registers DbContext as scoped by default, which is why database work follows the request boundary without extra configuration.

Singleton

A singleton is created once and shared by every consumer for the lifetime of the service provider. It can hold configuration, caches, or long-lived clients. Because many requests can use it at the same moment, its code must be thread-safe, and anything it stores is visible to all requests for the life of the application.

Blazor is different

Do not describe scoped services in Blazor as “per HTTP request.” Microsoft’s guidance ties scoped lifetime in Blazor to the hosting model. In Blazor Server, a scope corresponds to the circuit (the long-lived connection between the browser and the server), not to individual HTTP requests. Check the Blazor section of the Microsoft Learn dependency injection article for the model that applies to your project before deciding which lifetime to use.

Scope boundaries and the captive dependency error

A scope is the container’s way of marking where scoped instances live and when they are disposed. Microsoft’s guidance on service lifetimes states the rule directly:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A scoped service should always be used from within a scope–either an implicit scope (such as ASP.NET Core’s per-request scope) or an explicit scope created with IServiceScopeFactory.CreateScope().

Why a singleton cannot hold a scoped service

Suppose a singleton asks for a scoped IOrderRepository in its constructor. The singleton is created once, so it keeps that one repository instance forever. The repository was meant to live for a single request, but now it outlives its scope and may still hold a database connection or request-specific data. This is called a captive dependency.

When scope validation is enabled, ASP.NET Core rejects the registration at resolution time with an error similar to:

InvalidOperationException: Cannot consume scoped service 'Store.IOrderRepository'
from singleton 'Store.PricingCache'.

Scope validation is enabled by default in the Development environment. It is not enabled in Production unless you turn it on, so a captive dependency can go unnoticed until it causes a problem under real load.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Fixing it with IServiceScopeFactory

When a singleton or background component genuinely needs scoped work, inject IServiceScopeFactory and create a scope for each unit of work:

public class PricingCache(IServiceScopeFactory scopeFactory)
{
    public async Task RefreshAsync()
    {
        using var scope = scopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
        // Use repository here. It is disposed when the scope is disposed.
    }
}

The singleton itself stays singleton. Only the work it does is scoped, and each call gets its own repository that is released at the end of the block.

Constructor injection over service lookup

Constructor injection should be the default for application code. A constructor lists exactly what a class needs, and tests can pass in fakes without a container. The alternative, calling HttpContext.RequestServices.GetService inside a class, hides dependencies and makes the class harder to test. Microsoft’s guidance lists this as a practice to avoid in ordinary application code.

A long constructor is a design signal. If a class asks for eight dependencies, it probably has too many responsibilities. Splitting the class is usually a better fix than moving the lookups into a service locator.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Injecting scoped services into middleware

Conventional middleware is created once, when the pipeline is built, and reused for every request. Its constructor therefore behaves like singleton activation. Passing a scoped service into that constructor is a captive dependency in disguise.

Conventional middleware: inject into InvokeAsync

Middleware receives scoped services through the InvokeAsync parameters. The framework resolves those parameters from the current request’s scope on each call:

public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context, ITenantContext tenant)
    {
        // tenant is resolved for this request only.
        await _next(context);
    }
}

// Program.cs
builder.Services.AddScoped<ITenantContext, TenantContext>();
app.UseMiddleware<TenantMiddleware>();

Factory-based middleware with IMiddleware

A middleware class that implements IMiddleware is activated from the container for each request, so its dependencies can be scoped. Register the class in the container and then add it to the pipeline:

public class AuditMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Access scoped services through constructor parameters here.
        await next(context);
    }
}

// Program.cs
builder.Services.AddScoped<AuditMiddleware>();
app.UseMiddleware<AuditMiddleware>();

Keyed services

Keyed services let you register several implementations of one interface and select them by key. Keyed registration is available through AddKeyedSingleton, AddKeyedScoped, and AddKeyedTransient, and it applies to ASP.NET Core in .NET 8 and later. The Microsoft .NET 10 documentation describes the same pattern:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
builder.Services.AddKeyedScoped<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedScoped<IPaymentGateway, PayPalGateway>("paypal");

public class CheckoutService([FromKeyedServices("stripe")] IPaymentGateway gateway)
{
    // gateway is the StripeGateway instance.
}

Keyed lifetimes follow the same rules as unkeyed ones, so the captive dependency guidance above applies.

Best Value
Sale
Programming ASP.NET Core (Developer Reference)
  • Applying all key ASP.NET Core components, including MVC for HTML generation, .NET Core, EF Core, ASP.NET Identity, dependency injection, and more
  • Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap
  • ASP.NET Core code for implementing business logic and data transformations
  • Handling configuration, routing, controllers, views, and common tasks (including posting forms and presenting data)
  • Performing complementary tasks: error handling, logging, application design, authentication, localization, and more
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Background work with BackgroundService

A hosted BackgroundService is a singleton for the life of the application. It must not take a scoped service in its constructor. Instead, create a scope for each cycle of work and dispose it when the cycle ends:

public class ReportWorker(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var reports = scope.ServiceProvider.GetRequiredService<IReportService>();
                await reports.GenerateAsync(stoppingToken);
            }

            await Task.Delay(TimeSpan.FromMinutes(15), stoppingToken);
        }
    }
}

Disposal

The container disposes the services it creates, according to their registration and scope. Scoped services are disposed when their scope ends, and singletons are disposed when the application shuts down. Do not call Dispose on an injected service yourself, because the container will dispose it again or use it after you have released it.

Transient disposables need extra care. The container keeps a reference to each transient IDisposable it creates so it can dispose it later. If you resolve a transient disposable from the root provider, for example inside a singleton, that instance is held until the application stops. Resolve disposable transients inside a scope instead.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Development checks

Scope validation and build-time validation help catch registration mistakes early. In the Development environment, the ASP.NET Core host enables both by default. Their effect is to surface a captive dependency or a missing registration when the app starts or when the service is first resolved, not during a user’s request. Keep these checks enabled in development, and do not rely on them to find problems in Production, where they are off unless configured.

When to replace the built-in container

The built-in container covers constructor injection, the three lifetimes, factories, keyed services, and scope validation. Replace it only when your project needs a feature it lacks. Microsoft’s dependency injection guidelines list property injection, child containers, custom lifetime management, and convention-based registration as examples. Swapping containers adds configuration and team knowledge, so it should solve a concrete problem you have already hit.

Troubleshooting common DI errors

Symptom Likely cause Fix
InvalidOperationException about a scoped service consumed from a singleton A singleton or hosted service takes a scoped dependency directly Change the consumer’s lifetime, or create a scope with IServiceScopeFactory for the scoped work
Unable to resolve service for type … while attempting to activate … The requested type was never registered, or the registration uses a different interface Register the exact type the constructor asks for in Program.cs
Request-specific data appears in another request Per-request state is stored in a singleton or a static field Move the state into a scoped service, or pass it as a method parameter
Scoped service is stale or unexpectedly shared inside middleware The scoped service was injected into the middleware constructor Move the parameter to InvokeAsync, or use IMiddleware
Disposable objects accumulate in memory over time Transient disposables are resolved from the root provider Resolve them inside a scope so they are released when it ends

Checklist before you ship

  • Every registration lives in Program.cs or in an extension method that Program.cs calls.
  • No singleton, hosted service, or static field holds a scoped or transient dependency directly.
  • Each middleware class takes scoped dependencies in InvokeAsync or uses IMiddleware.
  • Singletons are thread-safe and store nothing specific to one user or request.
  • Classes declare dependencies in their constructors rather than looking them up.
  • Development runs with scope validation on, so captive dependencies fail fast.

For the exact behavior of each API and its current code samples, consult the Microsoft Learn guide to using dependency injection, and check the version selector on the docs site when your project targets an earlier .NET release.

Quick Recap

Bestseller No. 2
SaleBestseller No. 5
Programming ASP.NET Core (Developer Reference)
Programming ASP.NET Core (Developer Reference)
Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap; ASP.NET Core code for implementing business logic and data transformations
$24.99

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.