C# DI: Transient vs Scoped vs Singleton Lifetimes

In .NET dependency injection, service lifetimes determine how instances are created, reused, and disposed of by the inversion of control (IoC) container. This article explains the technical differences between the three primary lifetimes—Transient, Scoped, and Singleton—detailing their instance behavior, typical use cases, and key architectural pitfalls such as captive dependencies.

What Are Service Lifetimes?

When registering services in the .NET IServiceCollection, you specify a lifetime that instructs the runtime when to create a new instance of a dependency and when to reuse an existing one. Choosing the correct lifetime ensures memory efficiency, prevents unintended state sharing, and maintains thread safety.


1. Transient (AddTransient)

Transient services are created every single time they are requested from the dependency injection container. If five different components request the same transient dependency during a single web request, five distinct instances will be instantiated.

  • Lifetime: Minimal. Exists only as long as the requesting component needs it.
  • Instance Sharing: None. Every resolution produces a new instance.
  • Disposal: Disposed automatically at the end of the containing scope if the service implements IDisposable.
  • Best Used For:
    • Lightweight, stateless services.
    • Helper classes, algorithmic utilities, and mapper services.
    • Operations where no shared state should persist across consumers.
builder.Services.AddTransient<IEmailValidator, EmailValidator>();

2. Scoped (AddScoped)

Scoped services are created once per request (or per logical scope). Within the boundary of a single HTTP request in ASP.NET Core, every component that asks for a scoped service will receive the exact same instance. Once the request terminates, the scope ends, and the instance is disposed.

  • Lifetime: Boundary of the execution scope (e.g., one HTTP request).
  • Instance Sharing: Shared across all components resolving the dependency within the same scope, but isolated from other requests.
  • Disposal: Disposed at the end of the request/scope lifecycle.
  • Best Used For:
    • Stateful operations tied to a specific request.
    • Data access contexts, such as Entity Framework Core's DbContext.
    • Unit-of-work patterns and user-specific request handlers.
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

3. Singleton (AddSingleton)

Singleton services are created the first time they are requested (or when the application starts, if registered with an explicit instance) and stay alive for the entire duration of the application process. Every subsequent request, across all users and threads, receives the same instance.

  • Lifetime: Application lifetime.
  • Instance Sharing: Shared globally across the entire application.
  • Disposal: Disposed only when the application completely shuts down.
  • Best Used For:
    • In-memory caching layers.
    • Application-wide configuration objects.
    • Resource-heavy objects that are expensive to create (e.g., internal connection pools, third-party client wrappers).
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

Warning on Thread Safety: Because a Singleton is accessed concurrently by multiple threads handling simultaneous web requests, any mutable state inside a Singleton must be strictly thread-safe.


Comparison Summary

Lifetime Method Instances Created Ideal Use Case
Transient AddTransient<T> Every time requested Stateless operations, lightweight utilities
Scoped AddScoped<T> Once per HTTP request / scope Entity Framework DbContext, request-specific state
Singleton AddSingleton<T> Once per application runtime Caching, configurations, thread-safe global services

The Captive Dependency Pitfall

A common error in C# dependency injection is injecting a shorter-lived service into a longer-lived service.

For example, injecting a Scoped service (like DbContext) into a Singleton service turns the scoped service into a "captive dependency." The Singleton holds onto that single DbContext instance for the life of the entire application, bypassing the request-based boundary. This causes memory leaks, concurrency conflicts, and stale data.

To prevent this:

  • Never inject Scoped services into Singleton services.
  • Avoid injecting Transient services with disposable native resources into Singleton services.
  • In ASP.NET Core, dependency validation is enabled by default in the Development environment, throwing an InvalidOperationException if a captive dependency is detected at startup.