Using the wrong DI lifetime is the #1 cause of concurrency bugs in ASP.NET Core. We revisit Singleton, Scoped, and Transient with a focus on thread safety.
The Three Lifetimes
| Lifetime | Created… | Thread Safety |
|---|---|---|
| Transient | Every time requested | Safe (Instance per usage) |
| Scoped | Once per HTTP Request | Safe (Single thread per request) |
| Singleton | Once per App Lifetime | UNSAFE (Shared across all requests) |
The Captive Dependency Problem
Injecting a Scoped service into a Singleton service is a fatal error. The Scoped service will stay alive forever (captured), possibly holding onto a DbConnection.
// ❌ BAD
public class SingletonService
{
private readonly ScopedService _scoped; // Captured!
public SingletonService(ScopedService scoped) => _scoped = scoped;
}
// ✅ GOOD (IServiceScopeFactory)
public class SingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public void DoWork()
{
using var scope = _scopeFactory.CreateScope();
var scoped = scope.ServiceProvider.GetRequiredService<ScopedService>();
scoped.DoThing();
}
}
Key Takeaways
- EF Core `DbContext` is **Scoped**. Never inject it into a Singleton.
- Use `ValidateScopes = true` in development to catch these bugs early.
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.