The Three Lifetimes
ASP.NET Core's built-in dependency injection container supports three service lifetimes registered via AddSingleton, AddScoped, and AddTransient. Singleton services are created once for the entire application lifetime and every consumer receives the same instance, making them suitable for stateless, thread-safe services like configuration readers or in-memory caches. Scoped services are created once per scope, which in a web application means once per HTTP request — every service resolved within that request that depends on the same scoped type gets the identical instance. Transient services are created fresh every single time they are requested from the container, even multiple times within the same request.
Cricket analogy: Singleton is like the single official match ball used throughout an entire Test innings — everyone on the field shares that exact ball; Scoped is like the toss result, decided once per match and referenced by everyone that match; Transient is like a fresh pair of batting gloves put on before every single delivery.
The practical consequence of these lifetimes is state sharing and thread safety. A Singleton is instantiated once and must be safe for concurrent access from multiple simultaneous requests, since ASP.NET Core handles many requests in parallel on the thread pool; mutable state in a Singleton without proper locking is a classic source of race conditions. Scoped services are safe to hold per-request state — like a DbContext's change tracker — because .NET guarantees no two concurrent requests share the same scope. Transient services are the safest default for stateless, lightweight logic since a new instance means there's nothing to share or corrupt, but creating many transient instances of expensive-to-construct objects can add unnecessary allocation overhead.
Cricket analogy: A Singleton with unguarded mutable state is like two fielders trying to grab the same live ball at once without communicating — without a clear protocol (locking), you get a collision; a Scoped service is safer, like each fielder having their own zone for that over.
Captive Dependencies and Common Pitfalls
A captive dependency occurs when a longer-lived service holds a reference to a shorter-lived one, most commonly a Singleton injecting a Scoped service directly through its constructor. Because the Singleton is constructed once, it would capture that first request's Scoped instance and keep using it forever, silently breaking the per-request isolation the Scoped lifetime promises — .NET's DI container actually throws an InvalidOperationException at resolution time in the default validation configuration to catch this. The correct fix is to inject IServiceScopeFactory into the Singleton and create a fresh scope, and resolve the Scoped dependency from that scope, whenever the Singleton needs to do per-operation work.
Cricket analogy: A captive dependency is like a franchise's permanent head coach (Singleton) accidentally locking in one season's playing XI (Scoped) forever instead of letting the squad rotate each new season — the fix is having the coach draft a fresh XI list (new scope) every season.
// Program.cs registrations
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<ICourseRepository, CourseRepository>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
// Correct pattern for a Singleton that needs Scoped work
public class UsageMetricsAggregator : IHostedService
{
private readonly IServiceScopeFactory _scopeFactory;
public UsageMetricsAggregator(IServiceScopeFactory scopeFactory)
=> _scopeFactory = scopeFactory;
public async Task StartAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ICourseRepository>();
await repo.RecalculateEnrollmentCountsAsync(ct);
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
Enabling ValidateScopes and ValidateOnBuild in the host builder (the default in the Development environment) makes the DI container throw at startup or first resolution if a Singleton captures a Scoped or Transient dependency. Keep these validations on in Development so captive-dependency bugs surface immediately instead of silently corrupting production state.
Choosing the Right Lifetime
As a practical rule, register stateless, thread-safe services as Singleton to avoid repeated construction cost, register anything wrapping per-request state — like EF Core's DbContext, or a service that itself depends on a Scoped type — as Scoped, and reserve Transient for lightweight, cheap-to-construct helpers where uniqueness genuinely matters, such as a service that must not accidentally share mutable fields across concurrent uses within the same request.
Cricket analogy: Choosing lifetimes is like a captain deciding which decisions are made once per series (Singleton, e.g. team strategy), which reset every match (Scoped, e.g. the toss and playing XI), and which happen fresh every single ball (Transient, e.g. field placement tweaks).
- Singleton services are created once for the app's lifetime and shared across all requests concurrently.
- Scoped services are created once per HTTP request; every dependency resolved within that request shares the same instance.
- Transient services are created fresh every time they're resolved, even multiple times in one request.
- Singletons must be thread-safe since ASP.NET Core serves many requests concurrently on the thread pool.
- A captive dependency happens when a Singleton injects a Scoped service directly, freezing it to the first request forever.
- Use IServiceScopeFactory to safely resolve Scoped services from within a Singleton or background service.
- ValidateScopes/ValidateOnBuild (default in Development) catch captive-dependency mistakes at startup or resolution time.
Practice what you learned
1. How often is a new instance created for a service registered with AddScoped?
2. What is a captive dependency?
3. What is the recommended way for a Singleton to safely use a Scoped service?
4. Why must Singleton services generally be thread-safe?
5. What happens by default in Development when a Singleton captures a Scoped dependency through direct constructor injection?
Was this page helpful?
You May Also Like
Entity Framework Core Integration
How to wire Entity Framework Core into an ASP.NET Core application, from DbContext registration to migrations and query patterns.
Background Services with IHostedService
How to run long-lived and scheduled background work in ASP.NET Core using IHostedService and the BackgroundService base class.
Repository Pattern in ASP.NET Core
How to abstract data access behind repository interfaces in ASP.NET Core, and when that abstraction is worth the added indirection over EF Core directly.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics