What is a captive dependency?
Quick Answer
A captive dependency is when a longer-lived service holds a reference to a shorter-lived one, “capturing” it and keeping it alive far longer than intended. The classic case is a Singleton that injects a Scoped service: it grabs the first request's instance and reuses it for every request forever, causing stale data, memory growth and threading bugs.
A captive dependency is when something long-lived holds on to something short-lived. The usual example is a singleton that takes a scoped DbContext in its constructor. The singleton is built once, so it captures the very first DbContext and keeps using it for the life of the app — across every request and every thread. That gives you stale data, concurrency exceptions and memory that never gets released. ASP.NET Core actually catches this in development with scope validation and throws “cannot consume scoped service from singleton”. The fix is to inject IServiceScopeFactory into the singleton and create a fresh scope each time you need the scoped service, or use a factory like IDbContextFactory.
Each service is created according to its lifetime, but once created, the reference is held by whatever injected it. So if a Singleton injects a Scoped AppDbContext, that one context instance is now a field on an object that lives forever.
Consequences:
- Stale / wrong data — the captured scoped service keeps state from whenever it was first created.
- Thread-safety violations — a scoped service assumed single-request is now hit by every concurrent request.
- Leaks — a transient
IDisposablecaptured by a singleton is never disposed until shutdown.
ASP.NET Core guards against this: scope validation is on by default in the Development environment, and the host throws at startup or first resolve with “Cannot consume scoped service '...' from singleton '...'”. In production you can enable it explicitly with ValidateScopes = true / ValidateOnBuild = true.
Fixes:
- Inject
IServiceScopeFactory(orIServiceProvider) into the singleton and create a scope per operation. - Use a factory —
IDbContextFactory<T>, or aFunc<T>registration. - Reconsider the lifetimes — maybe the “singleton” should be scoped.
// Throws: 'Cannot consume scoped service AppDbContext from singleton ReportCache'
builder.Services.AddSingleton<ReportCache>();
builder.Services.AddScoped<AppDbContext>();
public class ReportCache
{
public ReportCache(AppDbContext db) { } // captive!
}
// Fix: resolve the scoped service on demand
public class ReportCache
{
private readonly IServiceScopeFactory _scopes;
public ReportCache(IServiceScopeFactory scopes) => _scopes = scopes;
public Report Build()
{
using var scope = _scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return /* build report from db */;
}
}
Practise the whole thread the way an interviewer walks it — each question builds on the last.