es, configuration providers, and factory interfaces.
- Scoped: One instance per logical request or operation. Required for
DbContext, HTTP context wrappers, and request-bound repositories.
- Transient: New instance per resolution. Suitable for lightweight, stateless processors.
When designing a service, audit its constructor parameters. If the service is registered as singleton, every injected dependency must also be singleton, or must be a factory interface capable of creating scopes on demand.
Step 2: Implement Scope Isolation for Long-Lived Consumers
Background services, hosted workers, and caching layers often require access to scoped infrastructure. Instead of injecting scoped services directly, inject IServiceScopeFactory and create explicit scopes per operation.
public interface IInventoryProcessor
{
Task ProcessBatchAsync(IList<int> productIds, CancellationToken ct);
}
public class InventoryProcessor : IInventoryProcessor
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<InventoryProcessor> _logger;
public InventoryProcessor(IServiceScopeFactory scopeFactory, ILogger<InventoryProcessor> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task ProcessBatchAsync(IList<int> productIds, CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var warehouseContext = scope.ServiceProvider.GetRequiredService<IWarehouseContext>();
var cacheProvider = scope.ServiceProvider.GetRequiredService<ICacheProvider>();
foreach (var productId in productIds)
{
var product = await warehouseContext.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == productId, ct);
if (product is not null)
{
var dto = new ProductSnapshotDto
{
Id = product.Id,
StockLevel = product.StockLevel,
LastUpdated = DateTimeOffset.UtcNow
};
await cacheProvider.SetAsync($"inv:{productId}", dto, TimeSpan.FromMinutes(10), ct);
}
}
}
}
Architecture Rationale:
IServiceScopeFactory is a singleton-safe interface that creates disposable scopes on demand.
AsNoTracking() detaches entities from the change tracker, preventing EF Core from holding onto disposed context references.
- DTO projection (
ProductSnapshotDto) ensures cached data contains no infrastructure dependencies.
- The
using statement guarantees scope disposal, releasing database connections and memory immediately after batch processing.
Step 3: Enforce HttpClient Lifecycle Management
Direct instantiation of HttpClient causes socket exhaustion and DNS staleness. The .NET runtime provides IHttpClientFactory to manage connection pooling and lifecycle rotation.
public interface IExternalCatalogClient
{
Task<HttpResponseMessage> FetchInventoryAsync(string sku, CancellationToken ct);
}
public class ExternalCatalogClient : IExternalCatalogClient
{
private readonly IHttpClientFactory _clientFactory;
public ExternalCatalogClient(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<HttpResponseMessage> FetchInventoryAsync(string sku, CancellationToken ct)
{
var client = _clientFactory.CreateClient("CatalogApi");
return await client.GetAsync($"api/v1/inventory/{sku}", ct);
}
}
Why this works: IHttpClientFactory pools underlying HttpMessageHandler instances, preventing socket exhaustion while allowing DNS updates and configuration changes to propagate without restarting the application.
Step 4: Validate Lifetimes at Startup
Enable scope validation in development and staging environments to catch mismatches before deployment.
builder.Services.AddDbContext<IWarehouseContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("WarehouseDb")));
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient("CatalogApi", client =>
{
client.BaseAddress = new Uri("https://catalog.internal/");
client.Timeout = TimeSpan.FromSeconds(10);
});
builder.Services.AddScoped<IInventoryProcessor, InventoryProcessor>();
builder.Services.AddHostedService<InventoryBackgroundWorker>();
// Enable strict lifetime validation
builder.Host.UseServiceProviderFactory(new DefaultServiceProviderFactory(
new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true }));
Rationale: ValidateScopes = true throws an exception at resolution time if a singleton attempts to resolve a scoped service. ValidateOnBuild = true performs this check during application startup, failing fast before any request is processed.
Pitfall Guide
1. The Cached Entity Trap
Explanation: Storing EF Core tracked entities in IMemoryCache or distributed caches. The cache outlives the DbContext, but the entity graph retains references to the change tracker and database connection. Subsequent requests read stale data or trigger ObjectDisposedException when the tracker attempts to lazy-load navigation properties.
Fix: Always project to DTOs or use AsNoTracking() before caching. Ensure cached objects contain only primitive or serializable types.
2. Background Service Scope Leakage
Explanation: Injecting IRepository or DbContext directly into BackgroundService or IHostedService. These services are singletons by construction. The injected scoped dependency is captured for the application lifetime, causing connection leaks and cross-operation state pollution.
Fix: Inject IServiceScopeFactory. Create a new scope at the beginning of each execution cycle and dispose it at the end. Never hold scoped references as class-level fields in hosted services.
3. HttpClient Socket Exhaustion
Explanation: Instantiating new HttpClient() per request or reusing a single static instance without factory management. Each instantiation opens a new TCP socket that remains in TIME_WAIT state. Rapid requests exhaust the ephemeral port range. Static instances cache DNS entries indefinitely, failing to reflect infrastructure changes.
Fix: Use IHttpClientFactory with named or typed clients. Configure PooledConnectionLifetime and DnsRefreshTimeout to control pool rotation and DNS staleness.
4. Static Service Locator Anti-Pattern
Explanation: Using IApplicationBuilder.ApplicationServices or static IServiceProvider fields to resolve scoped services from static helpers or middleware. This bypasses the request pipeline's scope management, capturing scoped dependencies in static memory.
Fix: Pass dependencies explicitly through method parameters or constructor injection. If static access is unavoidable, resolve IServiceScopeFactory and create explicit scopes within the static method.
5. AI-Generated Constructor Bloat
Explanation: AI assistants adding constructor parameters without auditing lifetime compatibility. The generated code compiles and passes tests but violates architectural boundaries, introducing silent state sharing.
Fix: Implement AI rule sets that enforce constructor parameter lifetime auditing. Require explicit lifetime comments in DI registrations. Use static analyzers to flag singleton-to-scoped dependencies during code review.
6. Transient-to-Singleton Capture
Explanation: Similar to scoped leakage, but with transient dependencies. Each resolution creates a new instance, but the singleton holds references to all resolved instances, causing unbounded memory growth.
Fix: Apply the same scope isolation pattern. If the singleton needs transient behavior, inject a factory interface that creates instances on demand rather than capturing them.
7. Misunderstanding AddDbContext Defaults
Explanation: Assuming DbContext is transient or singleton. The default registration is scoped, meaning one instance per request. Overriding this without understanding change tracker implications breaks EF Core's unit-of-work pattern.
Fix: Keep DbContext scoped by default. If cross-request caching is required, detach entities or use compiled queries with DTO projection. Never share DbContext instances across requests.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Caching frequent read operations | DTO projection + IMemoryCache with AsNoTracking() | Prevents change tracker leakage, ensures serializable cache payloads | Low: Minimal CPU overhead for projection, high ROI in reduced DB load |
| Background processing with DB access | IServiceScopeFactory + per-cycle scope creation | Guarantees disposal, prevents connection pool exhaustion | Medium: Slight allocation overhead per cycle, eliminates memory leaks |
| External API calls | IHttpClientFactory with named clients + PooledConnectionLifetime | Manages socket lifecycle, enables DNS rotation, prevents port exhaustion | Low: Factory overhead is negligible, prevents catastrophic pool failure |
| Request validation middleware | Constructor injection of scoped validators | Maintains request-bound state, aligns with pipeline lifecycle | Low: Standard DI pattern, zero additional infrastructure cost |
| Static utility requiring DI access | IServiceScopeFactory resolution within method scope | Avoids static capture, maintains disposal guarantees | Medium: Requires explicit scope management, prevents silent leaks |
Configuration Template
// Program.cs - DI Registration with Lifetime Enforcement
var builder = WebApplication.CreateBuilder(args);
// Infrastructure: Singleton-safe factories and caches
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient("WarehouseApi", options =>
{
options.BaseAddress = new Uri("https://warehouse.internal/");
options.Timeout = TimeSpan.FromSeconds(15);
options.PooledConnectionLifetime = TimeSpan.FromMinutes(5);
});
// Data Access: Scoped by default, explicit configuration
builder.Services.AddDbContext<IWarehouseContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("WarehouseDb"))
.EnableSensitiveDataLogging(false));
// Application Services: Explicit lifetime declarations
builder.Services.AddScoped<IInventoryService, InventoryService>();
builder.Services.AddScoped<IProductValidator, ProductValidator>();
builder.Services.AddSingleton<IInventoryProcessor, InventoryProcessor>();
// Hosted Services: Singleton by construction, scope isolation required
builder.Services.AddHostedService<InventoryBackgroundWorker>();
// Strict lifetime validation for non-production environments
if (!builder.Environment.IsProduction())
{
builder.Host.UseServiceProviderFactory(new DefaultServiceProviderFactory(
new ServiceProviderOptions
{
ValidateScopes = true,
ValidateOnBuild = true
}));
}
var app = builder.Build();
app.MapControllers();
app.Run();
Quick Start Guide
- Identify Long-Lived Consumers: Scan your codebase for classes registered as singleton or hosted as
IHostedService. List all constructor parameters and verify their registered lifetimes.
- Inject Scope Factories: Replace direct scoped dependency injection with
IServiceScopeFactory. Create explicit scopes within method bodies, wrap operations in using statements, and dispose scopes immediately after use.
- Detach Cached State: Audit all cache write operations. Ensure EF Core entities are projected to DTOs or queried with
AsNoTracking(). Verify cached objects contain no infrastructure dependencies or navigation properties.
- Enable Validation: Add
ValidateScopes = true and ValidateOnBuild = true to your service provider factory configuration. Run the application in development to catch mismatches before deployment.
- Enforce via Tooling: Configure AI assistants or IDE extensions to flag singleton-to-scoped constructor patterns. Implement pre-commit hooks that run lifetime validation analyzers on changed files.
Dependency injection lifetime management is not a runtime concern—it is an architectural contract. By enforcing scope isolation, detaching cached state, and validating registrations at startup, teams eliminate silent data corruption, prevent resource exhaustion, and establish a foundation for reliable AI-assisted development. The cost of enforcement is measured in minutes; the cost of leakage is measured in production incidents.