The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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.
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.
#1 Best Overall
| 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.
Recommended Free Tools
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.
Rank #2
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:
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.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →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.
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:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutebuilder.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
- 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
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.
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
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.




