Dependency injection is central to modern .NET applications. It manages how services are created and supplied to their consumers. However, choosing the wrong service lifetime can cause subtle bugs involving shared state, disposed objects, and database contexts.
ASP.NET Core supports three common lifetimes: transient, scoped, and singleton.
Understand the three lifetimes
Transient: A new instance is created each time the service is requested from the container.
Scoped: One instance is created per scope. In a typical ASP.NET Core request, the request establishes a scope.
Singleton: One instance is created and reused for the lifetime of the application service provider.
Registration examples:
builder.Services.AddTransient<IFormatter, Formatter>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IClock, SystemClock>();
These registrations are illustrative. The correct lifetime depends on the service’s dependencies, state, and intended behavior.
Avoid injecting scoped services into singletons
A common mistake is injecting a scoped service, such as an Entity Framework Core DbContext, into a singleton.
The singleton may retain that dependency far beyond the intended request scope. This can cause incorrect behavior or lifetime validation errors.
If a singleton needs to perform scoped work, create an explicit scope through IServiceScopeFactory:
public sealed class BackgroundProcessor(
IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync(
CancellationToken cancellationToken)
{
await using var scope =
scopeFactory.CreateAsyncScope();
var service = scope.ServiceProvider
.GetRequiredService<IOrderService>();
await service.ProcessPendingAsync(
cancellationToken);
}
}
This example assumes the application defines IOrderService.ProcessPendingAsync. A long-running processor should also handle failures and cancellation appropriately.
Be careful with singleton state
Singleton services are shared. Mutable fields can therefore introduce concurrency problems if multiple requests access them simultaneously.
A singleton should not store request-specific information in instance fields. Use appropriate per-request services or explicit immutable data instead.
Understand scoped database contexts
Entity Framework Core registers its context as scoped by default through AddDbContext. This is generally appropriate for request-based operations because each request can use its own context instance.
Do not share a single DbContext concurrently across unrelated operations. It is not designed for concurrent use.
Test lifetime behavior
Tests can reveal lifetime problems by creating multiple scopes and checking which instances are reused. Enable dependency injection scope validation where appropriate, particularly during development.
Also test background services independently: their scope creation, cancellation, and error handling differ from ordinary HTTP request processing.
Conclusion
Choosing dependency injection lifetimes is about matching an object’s lifecycle to its responsibility. Use transient services for lightweight independent instances, scoped services for work tied to a scope, and singletons for application-wide services designed for safe sharing. Pay particular attention to background processing and mutable state, where lifetime mistakes can become production bugs.