Engineering Notes
Home/Languages/C#/Dependency Injection Lifetimes in .NET
Languages · C#

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.

💻 Languages● Intermediate⏱ 3 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 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:

csharp
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 IHttpClientFactory

2The Three Lifetimes

LifetimeOne instance per…Typical use
SingletonApplicationStateless services, caches, configuration, HttpClient factories. Must be thread-safe.
ScopedScope (an HTTP request in ASP.NET Core)DbContext, unit of work, per-request user context
TransientResolution (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.

text
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:

csharp
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ShopDb>();

4Advanced Registration

  • Multiple implementations: register several INotifiers and inject IEnumerable<INotifier>; the last registration wins for a single INotifier.
  • 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 inject IOptions<T> (singleton snapshot), IOptionsSnapshot<T> (per request) or IOptionsMonitor<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

  1. Captive dependencies (singleton → scoped DbContext).
  2. Non-thread-safe singletons with mutable state.
  3. Service locator (IServiceProvider injected everywhere) hiding dependencies.
  4. Resolving scoped services from the root provider in background services.
  5. Huge constructors: a sign the class does too much.

📋 Quick Reference

text
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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • How do you enable validation everywhere? ValidateScopes and ValidateOnBuild in 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

1. A singleton injects a scoped DbContext. What's the problem?
Captive dependency.
2. What lifetime does AddDbContext use by default?
One per request.
3. How does a BackgroundService use a scoped service?
Scope per unit of work.
4. IOptionsMonitor<T> gives you…
Reloadable config.