What DI in ASP.NET Core does (short)
The built-in DI container constructs and manages object graphs for you. When you register a service, you choose its lifetime — that decision determines when/how often the container creates an instance. Choosing the wrong lifetime usually results in captured dependencies, concurrency bugs, or memory / resource leaks — subtle runtime problems that are perfect weeds in senior interviews.
Lifetimes (what they mean & typical use)
- Transient (
services.AddTransient<T>())
New instance every time requested. Use for lightweight, stateless services or short-lived operations. - Scoped (
services.AddScoped<T>())
One instance per scope. In an ASP.NET Core request pipeline each HTTP request defines a scope. Use forDbContext, per-request state, unit-of-work patterns. - Singleton (
services.AddSingleton<T>())
One instance for the lifetime of the app. Must be thread-safe and stateless (or internally synchronized). Use for caching services, configuration holders, long-lived resources.
Concrete registrations (example)
services.AddTransient<IMailer, SmtpMailer>();
services.AddScoped<AppDbContext>(); // EF Core DbContext
services.AddSingleton<IMetrics, AppMetrics>();
Code language: HTML, XML (xml)
Wrong-lifetime scenarios (detailed) — what they look like, why they’re bad, and how to fix
1) Captive dependency: Scoped injected into Singleton
Code (wrong):
public class MySingleton
{
private readonly AppDbContext _db; // DbContext is scoped!
public MySingleton(AppDbContext db) => _db = db;
}
services.AddScoped<AppDbContext>();
services.AddSingleton<MySingleton>();
Code language: PHP (php)
What happens:
During singleton construction the container resolves AppDbContext from the root scope. That scoped service is effectively captured by the singleton and lives like a singleton — it is reused across requests. Consequences:
DbContextis not thread-safe → concurrent requests calling singleton methods that use_dbwill cause race conditions and unpredictable exceptions.- Per-request behavior (transactions, request identity) is lost — multiple users may inadvertently share the same context/transaction.
- Hard-to-debug data integrity and concurrency issues.
Fixes:
- Prefer not to capture a scoped service in a singleton. Instead resolve the scoped service per operation using a scope factory:
public class MySingleton
{
private readonly IServiceScopeFactory _scopeFactory;
public MySingleton(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public void DoWork()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// use db here safely per-operation
}
}
Code language: PHP (php)
- Or inject a factory
Func<AppDbContext>or a dedicated factory type that creates a newDbContextper call.
Interview line to say:
“Never inject scoped services into singletons directly — do per-operation resolution using IServiceScopeFactory or a factory delegate so each request gets its own instance.”
2) Singleton depends on stateful service (user-specific)
Code (wrong):
services.AddSingleton<UserSessionService>(); // holds current user's id in a field
Code language: JavaScript (javascript)
What happens:
Singletons are shared between requests. Storing request/user-specific data in singleton fields causes data leakage across requests — a catastrophic privacy bug.
Fix:
Make the service Scoped, or make the singleton stateless and accept user identity as a method parameter, or store per-user data in a per-request store (e.g., HttpContext.Items) or a distributed cache keyed by session id.
Interview line:
“Singletons must not store request-specific state. If you need per-request state, use scoped services or pass data into method calls.”
3) DbContext registered as Singleton
Code (wrong):
services.AddSingleton<AppDbContext>();
Code language: HTML, XML (xml)
Why wrong:DbContext assumes per-scope lifetime. Singletons share one instance across threads; EF Core DbContext is not thread-safe, leading to concurrency issues, stale tracking state, and often exceptions like InvalidOperationException when used simultaneously.
Fix:services.AddDbContext<AppDbContext>(ServiceLifetime.Scoped) — default is scoped.
Interview line:
“EF Core DbContext should always be scoped. Making it singleton breaks its internal change tracking and threading assumptions.”
4) Transient heavy object captured as singleton
Code (subtle bug):
services.AddTransient<HeavyParser>();
services.AddSingleton<SingletonService>(sp => new SingletonService(sp.GetRequiredService<HeavyParser>()));
Code language: HTML, XML (xml)
What happens:
The transient HeavyParser resolves once during the singleton factory and is reused forever — losing transient semantics. If HeavyParser holds per-call state or depends on other scoped services, you get misbehavior.
Fix:
Inject a factory or IServiceProvider and resolve per-call:
public SingletonService(Func<HeavyParser> parserFactory) { ... }
Code language: HTML, XML (xml)
or via IServiceProvider with create scope when needed.
5) Singleton HttpClient vs raw static HttpClient
Rule of thumb: use IHttpClientFactory (registered as singleton infrastructure) to manage HttpClient lifetimes and DNS rotation. Don’t manually use a new HttpClient per request or hold a stale one with old DNS.
Interview line:
“Use IHttpClientFactory to get configured HttpClient instances to avoid socket exhaustion and DNS stale issues.”
Patterns to resolve lifetime mismatches (practical options)
- IServiceScopeFactory.CreateScope() — create a scope inside a singleton method, resolve scoped services from that scope, dispose after use.
- Factory delegate —
services.AddTransient<Func<MyScopedService>>(sp => () => sp.GetRequiredService<MyScopedService>());injectFunc<MyScopedService>and call when needed. - Lazy / Func wrappers — delay creation until method call.
- Design change — make the singleton stateless and accept the scoped objects as parameters from caller (if caller is scoped).
- BackgroundService / IHostedService: if a background singleton worker needs scoped services, create a scope inside the worker loop for each unit of work:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// work with db
}
}
Code language: JavaScript (javascript)
Thread-safety and disposal notes
- Singletons must be thread-safe: any mutable state needs locking or concurrent structures. Immutable objects are ideal.
- Be careful with
IDisposableservices: the container disposes singletons when the provider is disposed (app shutdown); scoped disposables are disposed when the scope ends. - Avoid holding references to
IServiceProviderand resolving everything from it all the time — prefer explicit dependencies or small well-defined factories for clarity and testability.
Example: bad -> fixed end-to-end
Bad:
public class ReportGenerator
{
private readonly AppDbContext _db;
public ReportGenerator(AppDbContext db) => _db = db; // captured scoped into singleton
public IEnumerable<Report> Generate() => _db.Reports.ToList(); // concurrency hazard
}
services.AddScoped<AppDbContext>();
services.AddSingleton<ReportGenerator>();
Code language: PHP (php)
Fixed:
public class ReportGenerator
{
private readonly IServiceScopeFactory _scopeFactory;
public ReportGenerator(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public IEnumerable<Report> Generate()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return db.Reports.AsNoTracking().ToList();
}
}
services.AddScoped<AppDbContext>();
services.AddSingleton<ReportGenerator>();
Code language: PHP (php)How to explain this in an interview (concise script)
“Dependency injection lifetimes control how often instances are created. Transient: every request; Scoped: one per logical scope (HTTP request in web apps); Singleton: one for app lifetime. A frequent mistake is capturing a scoped service in a singleton — for example, injecting DbContext into a singleton — which effectively promotes the scoped instance to behave like a singleton. Because DbContext is not thread-safe and holds per-request tracking state, this results in concurrency bugs and stale or cross-request data. The proper patterns are: make the service scoped if it needs per-request data, or resolve scoped dependencies per operation using IServiceScopeFactory (or an injected factory) so each operation gets a fresh instance. Also prefer IHttpClientFactory for outbound HTTP, and avoid storing request-specific state in singletons.”
Quick checklist (for senior-level answers)
- State lifetimes and their intended uses.
- Describe captured dependency problem explicitly.
- Demonstrate a code fix:
IServiceScopeFactory.CreateScope()orFunc<T>factory. - Mention thread-safety and
DbContextspecifics. - Mention
IHttpClientFactoryand options pattern as best-practice. - Explain trade-offs (simplicity vs safety) and why your fix is safe for concurrency.