.claude/skills/dependency-injection-lifetime/SKILL.md
Use when choosing DI lifetimes, diagnosing a 'Cannot consume scoped service from singleton' error, or when a singleton is holding stale data from a captured DbContext. Covers Singleton vs Scoped vs Transient decision matrix, the captive dependency trap and how to detect it with ValidateScopes, IServiceScopeFactory for singletons that need scoped services, keyed services (.NET 8+), and common DI registration anti-patterns. Domain: Dependency Injection, Architecture. Level: Intermediate. Tags: dependency-injection, di, lifetime, scoped, singleton, captive-dependency.
npx skillsauth add klod68/littlerae dependency-injection-lifetimeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Choosing the wrong DI lifetime causes bugs that are subtle and hard to diagnose: stale data from singletons holding scoped state, concurrency corruption from shared mutable state, memory leaks from transient disposables never released, or the "captive dependency" trap where a long-lived service captures a short-lived one.
| Lifetime | Instance Count | Disposed When | Use For |
|----------|---------------|---------------|---------|
| Singleton | One per application | App shutdown | Stateless services, caches, HttpClient factories, configuration, thread-safe shared resources |
| Scoped | One per scope (HTTP request) | Scope end (request end) | DbContext, unit-of-work, per-request state, tenant context |
| Transient | New every resolve | Next GC (or scope end if IDisposable) | Lightweight stateless services, factories, short-lived utilities |
Is the service stateless and thread-safe?
├── Yes → Singleton (cheapest lifetime)
└── No → Does it hold per-request state (DbContext, user context)?
├── Yes → Scoped
└── No → Transient
A captive dependency occurs when a longer-lived service captures a shorter-lived dependency:
❌ Singleton → Scoped (singleton holds stale/shared DbContext)
❌ Singleton → Transient (transient becomes de-facto singleton)
✅ Scoped → Transient (safe — transient lives within scope)
✅ Scoped → Scoped (safe — same scope)
✅ Singleton → Singleton (safe — same lifetime)
.NET validates this at build time when ValidateScopes is enabled (default in Development):
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // Default in Development
options.ValidateOnBuild = true; // Catches missing registrations at startup
});
When a singleton needs to resolve scoped services, create an explicit scope:
internal sealed class OrderProcessor
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderProcessor> _logger;
public OrderProcessor(IServiceScopeFactory scopeFactory, ILogger<OrderProcessor> logger)
{
ArgumentNullException.ThrowIfNull(scopeFactory);
ArgumentNullException.ThrowIfNull(logger);
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task ProcessAsync(int orderId, CancellationToken ct)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var order = await dbContext.Orders.FindAsync([orderId], ct);
// ... process
}
}
Register multiple implementations of the same interface, resolved by key:
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
// Resolve:
public class OrderService([FromKeyedServices("email")] INotifier notifier) { }
// ✅ Register by interface
services.AddScoped<IOrderRepository, OrderRepository>();
// ❌ Register by concrete type
services.AddScoped<OrderRepository>();
// ❌ Registers two instances — IOrderService gets its own singleton
services.AddSingleton<OrderService>();
services.AddSingleton<IOrderService, OrderService>();
// ✅ Forward the interface to the same instance
services.AddSingleton<OrderService>();
services.AddSingleton<IOrderService>(sp => sp.GetRequiredService<OrderService>());
// Libraries use TryAdd — consumer registrations win
services.TryAddScoped<IOrderRepository, DefaultOrderRepository>();
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Singleton capturing DbContext | Stale/shared data, concurrency bugs | Use IServiceScopeFactory to create scope |
| Transient IDisposable without scope | Memory leak — never disposed | Use Scoped or manage disposal explicitly |
| Registering everything as Transient | Unnecessary allocations, no sharing | Use Singleton for stateless, thread-safe services |
| Scoped service in background worker | No HTTP scope exists — throws at runtime | Create scope via IServiceScopeFactory |
| Service locator (IServiceProvider.GetService in business logic) | Hidden dependencies, untestable | Inject dependencies via constructor |
| Missing ValidateOnBuild | Missing registrations discovered at runtime | Enable ValidateOnBuild = true in all environments |
| Multiple AddSingleton for same type | Two instances created — one per registration | Forward with sp.GetRequiredService<T>() |
| new DbContext() inside a service | Bypasses DI, no scope, no disposal | Inject via constructor or scope factory |
ILogger<T> is Singleton: Safe to inject anywhere — no captive dependency risk.IOptions<T> vs IOptionsSnapshot<T>: IOptions<T> is singleton (reads config once); IOptionsSnapshot<T> is scoped (re-reads per request). Use IOptionsMonitor<T> for singletons that need live reload.HttpClient via factory: Never register HttpClient directly — use AddHttpClient<T>() which manages handlers and lifetimes.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.