100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Service Lifetimes: Singleton, Scoped, Transient

How ASP.NET Core's built-in dependency injection container manages object lifetime through Singleton, Scoped, and Transient registrations.

Data & ServicesIntermediate9 min readJul 10, 2026
Analogies

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.

csharp
// 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

Was this page helpful?

Topics covered

#ASPNETCoreStudyNotes#MicrosoftTechnologies#ServiceLifetimesSingletonScopedTransient#Service#Lifetimes#Singleton#Scoped#StudyNotes#SkillVeris#ExamPrep