Three lifetimes, one rule: a service can only depend on something that lives at least as long as it does.
Inject a scoped service into a singleton and the singleton captures the first scope forever. With EF Core that means one DbContext shared across every request: stale change tracking, concurrency exceptions, and eventually a corrupted unit of work. The container catches this at startup only when validation is on.
builder.Services.AddDbContext<AppDb>(o => o.UseSqlServer(cs)); // scoped
builder.Services.AddSingleton<ScheduleCache>(); // singleton
// wrong: ScheduleCache(AppDb db)
// right: resolve a scope per unit of work
public class ScheduleCache(IServiceScopeFactory scopes)
{
public async Task WarmAsync()
{
using var scope = scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDb>();
// ...
}
}BackgroundService and Azure Functions with singleton hosts hit the same trap. Create a scope per message or per timer tick — that scope boundary is also the natural transaction boundary.
Turn on ValidateScopes and ValidateOnBuild in every environment, not just Development. The error at startup is cheaper than the bug in production.