Dependency Injection Lifetimes in .NET
Master .NET's built-in dependency injection: singleton, scoped and transient lifetimes, captive dependencies and scope validation, scopes in background services, keyed services and the options pattern, then build a container yourself.
🎯 What you'll learn
- Registering services in the built-in container
- Singleton, scoped and transient lifetimes
- Captive dependencies and how to avoid them
- Keyed services, factories and the options pattern
📚 Before you start
1The Built-in Container
ASP.NET Core ships with Microsoft.Extensions.DependencyInjection. You register services on builder.Services and the framework resolves them for controllers, minimal API handlers, middleware, hosted services and other services:
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();
builder.Services.AddDbContext<ShopDb>(o => o.UseNpgsql(cs)); // scoped by default
builder.Services.AddHttpClient<PaymentsClient>(); // typed client via IHttpClientFactory2The Three Lifetimes
| Lifetime | One instance per… | Typical use |
|---|---|---|
| Singleton | Application | Stateless services, caches, configuration, HttpClient factories. Must be thread-safe. |
| Scoped | Scope (an HTTP request in ASP.NET Core) | DbContext, unit of work, per-request user context |
| Transient | Resolution (every injection) | Lightweight stateless helpers |
Disposable services are disposed by the container at the end of their lifetime: scoped ones when the request ends, singletons at shutdown. Transient disposables resolved from the root provider live until shutdown, a subtle leak.
3Captive Dependencies
A service must not depend on a service with a shorter lifetime. A singleton that receives a scoped DbContext captures it for the life of the app: the context is shared across requests and threads (not thread-safe) and never disposed.
Singleton → Scoped ✗ captive dependency
Singleton → Transient ⚠ the transient becomes effectively a singleton
Scoped → Singleton ✓In Development, ASP.NET Core enables scope validation (ValidateScopes, ValidateOnBuild) and throws when a singleton resolves a scoped service. When a singleton (like a BackgroundService) genuinely needs scoped services, create a scope per unit of work:
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ShopDb>();4Advanced Registration
- Multiple implementations: register several
INotifiers and injectIEnumerable<INotifier>; the last registration wins for a singleINotifier. - Keyed services (.NET 8):
AddKeyedSingleton<IStorage, S3Storage>("s3")and[FromKeyedServices("s3")]. - Factories:
AddScoped<IRepo>(sp => new Repo(sp.GetRequiredService<ShopDb>(), "eu")). - Options pattern:
services.Configure<SmtpOptions>(config.GetSection("Smtp"))and injectIOptions<T>(singleton snapshot),IOptionsSnapshot<T>(per request) orIOptionsMonitor<T>(change notifications). - Decorators: wrap implementations for caching or logging (Scrutor helps with assembly scanning and decoration).
5Testing
Override registrations in tests with WebApplicationFactory<Program>.WithWebHostBuilder(b => b.ConfigureTestServices(s => s.AddSingleton<IClock>(fakeClock))). Constructor injection makes unit tests simple: pass fakes directly.
⚠️ Common Pitfalls
- Captive dependencies (singleton → scoped DbContext).
- Non-thread-safe singletons with mutable state.
- Service locator (
IServiceProviderinjected everywhere) hiding dependencies. - Resolving scoped services from the root provider in background services.
- Huge constructors: a sign the class does too much.
📋 Quick Reference
Singleton (app, thread-safe) · Scoped (per request: DbContext) · Transient (per resolution)
never depend on a shorter lifetime · ValidateScopes/ValidateOnBuild in dev
background services: IServiceScopeFactory.CreateScope() per unit of work
IEnumerable<T> for many impls · keyed services · IOptions / IOptionsSnapshot / IOptionsMonitor🧠 Key takeaways
- Singleton per app (thread-safe), scoped per request (DbContext), transient per resolution.
- Never depend on a shorter lifetime: that's a captive dependency.
- Background services create a scope per unit of work.
- Use IEnumerable<T>, keyed services and IOptions variants deliberately.
🎤 Interview Q&A
Q1Explain singleton, scoped and transient.core▾
Singleton: one instance for the app, must be thread-safe. Scoped: one per scope, i.e. per HTTP request in ASP.NET Core (DbContext). Transient: new instance every time it's injected. The container disposes instances at the end of their lifetime.
- Where are transient disposables a problem? When resolved from the root provider, they live until shutdown.
Q2What is a captive dependency?core▾
A longer-lived service holding a shorter-lived one, e.g. a singleton capturing a scoped DbContext, causing shared non-thread-safe state and no disposal. ASP.NET Core's scope validation detects it in Development.
- How do you enable validation everywhere?
ValidateScopesandValidateOnBuildin host options.
Q3How do you use scoped services in a hosted service?core▾
Inject IServiceScopeFactory, create a scope per message/iteration, resolve the scoped services from it, and dispose the scope when done.
- Why per iteration? So DbContexts and tracked entities don't grow forever.
Q4How do you register multiple implementations?core▾
Register each; inject IEnumerable<IService> to get all, or a single IService to get the last registered. .NET 8 keyed services select by key.
- Use case? Notification channels or payment providers chosen at runtime.
Q5IOptions vs IOptionsSnapshot vs IOptionsMonitor?senior▾
IOptions: singleton value read once. IOptionsSnapshot: scoped, recomputed per request (reload-aware). IOptionsMonitor: singleton with current value and change callbacks, usable in singletons.
- Validation?
ValidateDataAnnotations().ValidateOnStart().
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.