Introduction

ASP.NET Core DI validates service lifetimes to prevent captive dependencies: when a scoped service (like DbContext) is injected into a singleton service, the scoped instance lives for the entire application lifetime instead of being created per-request. Since .NET 6, this throws InvalidOperationException at startup by default.

Symptoms

  • InvalidOperationException: Cannot consume scoped service 'AppDbContext' from singleton 'CacheService'
  • Error occurs at application startup during service registration validation
  • DbContext accessed from a singleton background service
  • Works in development with validation disabled but fails in production
  • Error after changing a service from scoped to singleton

Example error: `` System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: ICacheService Lifetime: Singleton ImplementationType: CacheService': Cannot consume scoped service 'AppDbContext' from singleton 'CacheService'. ---> System.InvalidOperationException: Cannot consume scoped service 'AppDbContext' from singleton 'CacheService'.

Common Causes

  • Singleton service needs per-request data access (DbContext)
  • IHostedService (singleton) injects scoped services
  • Singleton caching service needs database access
  • Background worker needs scoped services
  • Misunderstanding of service lifetime relationships

Step-by-Step Fix

  1. 1.Use IServiceScopeFactory to create scoped instances:
  2. 2.```csharp
  3. 3.// WRONG: DbContext injected into singleton
  4. 4.public class CacheService : ICacheService
  5. 5.{
  6. 6.private readonly AppDbContext _context; // Captive dependency!

public CacheService(AppDbContext context) { _context = context; } }

// CORRECT: inject scope factory and create scope when needed public class CacheService : ICacheService { private readonly IServiceScopeFactory _scopeFactory;

public CacheService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }

public async Task<User> GetUserAsync(string id) { // Create a new scope for this operation using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>(); return await context.Users.FindAsync(id); } } ```

  1. 1.Change the service lifetime to match dependencies:
  2. 2.```csharp
  3. 3.// If the service only needs to live per-request, make it scoped
  4. 4.builder.Services.AddScoped<ICacheService, CacheService>();

// If it needs to be singleton, inject only singleton-scoped dependencies builder.Services.AddSingleton<ICacheService, CacheService>(); // And use IServiceScopeFactory inside CacheService for scoped access ```

  1. 1.Use factory pattern for scoped service creation:
  2. 2.```csharp
  3. 3.public interface IDbContextFactory
  4. 4.{
  5. 5.AppDbContext Create();
  6. 6.}

public class DbContextFactory : IDbContextFactory { private readonly IServiceScopeFactory _scopeFactory; private readonly ConcurrentBag<IDisposable> _scopes = new();

public DbContextFactory(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }

public AppDbContext Create() { var scope = _scopeFactory.CreateScope(); _scopes.Add(scope); return scope.ServiceProvider.GetRequiredService<AppDbContext>(); } }

// Register as singleton builder.Services.AddSingleton<IDbContextFactory, DbContextFactory>(); ```

  1. 1.Handle scoped services in BackgroundService:
  2. 2.```csharp
  3. 3.public class DataProcessingService : BackgroundService
  4. 4.{
  5. 5.private readonly IServiceScopeFactory _scopeFactory;

public DataProcessingService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }

protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Create a scope for each processing cycle using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

var pendingItems = await context.PendingItems.ToListAsync(stoppingToken); foreach (var item in pendingItems) { await ProcessItemAsync(item, context, stoppingToken); }

await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } } ```

Prevention

  • Keep ValidateScopes = true in all environments (it is the default)
  • Inject IServiceScopeFactory into singletons that need scoped services
  • Never inject scoped services directly into singletons
  • Use IHttpContextAccessor for per-request data in singletons
  • Document service lifetime requirements in interface comments
  • Use analyzers to detect lifetime mismatches at compile time