Cannot consume scoped service 'DiLab.IUnitOfWork' from singleton 'DiLab.ReportCache'.
— a class registered as a singleton takes a scoped service in its constructor. Either inject
IServiceScopeFactory and create a scope each time the singleton needs the scoped
service, or register the consumer as scoped too.
Which one is right depends on the consumer. If it really must live as long as the app, like
a cache or a background worker, keep it a singleton and create scopes. If it became a
singleton by habit, make it scoped. Making the dependency a singleton instead only works when
it holds no per-request state and is thread-safe, which rules out a DbContext.
The error
Thrown by builder.Build() in the Development environment, the app never starts:
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: DiLab.ReportCache Lifetime: Singleton ImplementationType: DiLab.ReportCache': Cannot consume scoped service 'DiLab.IUnitOfWork' from singleton 'DiLab.ReportCache'.)
---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: DiLab.ReportCache Lifetime: Singleton ImplementationType: DiLab.ReportCache': Cannot consume scoped service 'DiLab.IUnitOfWork' from singleton 'DiLab.ReportCache'.
---> System.InvalidOperationException: Cannot consume scoped service 'DiLab.IUnitOfWork' from singleton 'DiLab.ReportCache'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitCallSite(ServiceCallSite callSite, CallSiteValidatorState argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitConstructor(ConstructorCallSite constructorCallSite, CallSiteValidatorState state)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.VisitCallSite(ServiceCallSite callSite, CallSiteValidatorState argument)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
--- End of inner exception stack trace ---
at Microsoft.Extensions.DependencyInjection.ServiceProvider.ValidateService(ServiceDescriptor descriptor)
at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
--- End of inner exception stack trace ---
at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(ICollection`1 serviceDescriptors, ServiceProviderOptions options)
at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options)
at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
at Program.<Main>$(String[] args) in ...\DiLab\Program.cs:line 8
Only the file path in the last line is shortened; nothing else is changed.
Why it happens
A scoped service lives for one request and is disposed when the request ends. A singleton is created once and lives as long as the app. When a singleton takes a scoped service in its constructor, it keeps the first instance for good: every later request shares the object that was meant for one request, from many threads at once. This is called a captive dependency. In Development the host validates scopes and refuses to build. In Production it does not, and nothing throws. The lab's two requests in Production showed the same unit of work:
3 open reports (unit of work f9843cc7-739c-43ec-886b-87fdac820538)
3 open reports (unit of work f9843cc7-739c-43ec-886b-87fdac820538)
With a DbContext in that place, one context would serve every user of the app.
The fix
public class ReportCache(IServiceScopeFactory scopeFactory)
{
public string Summary()
{
using var scope = scopeFactory.CreateScope();
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
return $"{unitOfWork.CountOpenReports()} open reports (unit of work {unitOfWork.Id})";
}
}
The validator has no objection to IServiceScopeFactory in a singleton. Each call
now opens a scope, resolves a fresh IUnitOfWork from it, and disposes the scope
when the method returns. The app started in Development and two requests showed two
different unit-of-work IDs. The other fix, if ReportCache never needed to outlive a request,
is to change its registration and keep the original constructor:
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<ReportCache>();
How it was reproduced
A minimal API from dotnet new web -o DiLab on .NET SDK 10.0.401 (ASP.NET Core
runtime 10.0.12): an IUnitOfWork registered with AddScoped, a
ReportCache registered with AddSingleton whose constructor takes
IUnitOfWork, and a GET /reports endpoint. Started with
ASPNETCORE_ENVIRONMENT=Development it exited at builder.Build()
with the error above; started as Production it ran and returned the same ID twice. Both fixes
were run in Development and confirmed with two requests each.
Frequently asked
- What is a captive dependency in ASP.NET Core?
- A service with a short lifetime held by one with a longer lifetime, such as a scoped service inside a singleton. The short-lived instance is never replaced, so the object created for one request is reused by every later request.
- How do I use a scoped service inside a singleton or a BackgroundService?
- Inject IServiceScopeFactory, call CreateScope each time you need the scoped service, resolve it from scope.ServiceProvider, and dispose the scope when the work is done. Never store the resolved service in a field.
- Why does this error appear in Development but not in Production?
- Development validates scopes when the app is built; Production does not. The bug is still there in Production: the singleton keeps the first scoped instance for the life of the app, which the lab showed as the same unit of work on every request.
More decoded errors in the Fixes category. The three lifetimes are explained in Services and Dependency Injection in Blazor.