ASP.NET Core Web APIs · Minimal APIs Done Right

Dependency Injection Lifetimes and Captive Dependencies

Register services as singleton, scoped or transient, and avoid the captive dependency bug where a singleton holds a scoped service.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Three services with different lifetimes and an endpoint that reports which instances it received across two requests, plus the captive dependency mistake (a singleton holding a scoped service), which ASP.NET Core's scope validation catches at startup in development.

The idea

  • Singleton: one instance for the app. Scoped: one per request. Transient: a new one every time it's injected.
  • Database contexts and anything holding per-request state are scoped.
  • A singleton that depends on a scoped service captures the first instance forever: it's shared across requests and threads, a source of data leaks and threading bugs.
  • ValidateScopes (on by default in Development) and ValidateOnBuild detect captive dependencies when the app starts.

The code

Lifetimes across two requests, and a captive dependency caught at startup
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton<SingletonId>();
builder.Services.AddScoped<ScopedId>();
builder.Services.AddTransient<TransientId>();
var app = builder.Build();
app.MapGet("/ids", (SingletonId s, ScopedId a, ScopedId b, TransientId t1, TransientId t2) =>
    $"singleton={s.Id} scoped={a.Id}/{b.Id} transient={t1.Id}/{t2.Id}");
app.Urls.Add("http://127.0.0.1:0");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
Console.WriteLine("request 1: " + await http.GetStringAsync("/ids"));
Console.WriteLine("request 2: " + await http.GetStringAsync("/ids"));
await app.StopAsync();

var bad = WebApplication.CreateBuilder();
bad.Host.UseDefaultServiceProvider(o => { o.ValidateScopes = true; o.ValidateOnBuild = true; });
bad.Services.AddScoped<ScopedId>();
bad.Services.AddSingleton<ReportCache>();          // singleton that needs a scoped service
try { bad.Build(); }
catch (AggregateException e) { Console.WriteLine("startup check: " + e.InnerExceptions[0].Message.Split(": ")[^1]); }

class Counter { static int next; public static int Next() => Interlocked.Increment(ref next); }
class SingletonId { public int Id { get; } = Counter.Next(); }
class ScopedId { public int Id { get; } = Counter.Next(); }
class TransientId { public int Id { get; } = Counter.Next(); }
class ReportCache(ScopedId scoped) { public int Id => scoped.Id; }

Output

request 1: singleton=1 scoped=2/2 transient=3/4
request 2: singleton=1 scoped=5/5 transient=6/7
startup check: Cannot consume scoped service 'ScopedId' from singleton 'ReportCache'.

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Registering DbContext as a singleton (or injecting it into one): shared across requests, not thread-safe.
  • Disabling scope validation in production builds instead of fixing the registration.
  • Resolving scoped services from the root provider in background services; create a scope (IServiceScopeFactory.CreateScope()).

Key points

  • Singleton per app, scoped per request, transient per injection.
  • Never let a singleton depend on a scoped service.
  • Turn on ValidateOnBuild and ValidateScopes to catch mistakes at startup.

Further reading: Dependency injection in ASP.NET Core · Service lifetimes

Practice questions

What is a captive dependency?

Answer

A service with a longer lifetime holding one with a shorter lifetime, such as a singleton that receives a scoped DbContext and keeps using that first instance across requests.

Which lifetime should a DbContext have?

Answer

Scoped (one per request), which AddDbContext registers by default; DbContext isn't thread-safe and must not be shared.

How does a background service use scoped services?

Answer

It injects IServiceScopeFactory and creates a scope for each unit of work, resolving scoped services from that scope.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?