Cannot consume scoped service 'FixLab.IReportService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.
— a BackgroundService takes a scoped service in its constructor, and hosted
services are singletons. Inject IServiceScopeFactory instead, create a scope for
each piece of work, and resolve the scoped service from that scope.
The singleton in the message is not your class.
AddHostedService registers your worker as IHostedService with a
singleton lifetime, so the message names the interface and the service descriptor names your
class. Look for the scoped type it quotes in the constructors of your hosted services. Most
often it is a DbContext, which AddDbContext registers as scoped.
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: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: FixLab.ReportWorker': Cannot consume scoped service 'FixLab.IReportService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)
---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: FixLab.ReportWorker': Cannot consume scoped service 'FixLab.IReportService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.
---> System.InvalidOperationException: Cannot consume scoped service 'FixLab.IReportService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.
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 ...\FixLab\Program.cs:line 8
Only the file path in the last line is shortened; nothing else is changed.
Why it happens
A scoped service is meant to live for one scope, such as one HTTP request, and to be disposed
when that scope ends. The host creates each hosted service once, at startup, and keeps it for
the life of the app. A scoped service handed to its constructor is therefore never replaced:
the worker holds it captive. That is the bug the check exists to catch. The stack shows where
it ran: ServiceProvider.ValidateService, called while builder.Build()
constructed the service provider, with the rule itself in CallSiteValidator.
Development switches that validation on. Production does not, and the same app starts without
complaint while the worker logs the same instance on both iterations:
info: FixLab.ReportWorker[0]
Iteration 1: 3 open reports (report service 7a44e63c-7127-408d-8c55-5f88ed6ccb9c)
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: FixLab.ReportWorker[0]
Iteration 2: 3 open reports (report service 7a44e63c-7127-408d-8c55-5f88ed6ccb9c)
With a DbContext in that place, the worker would keep one context, and everything
it tracks, for as long as the app runs.
The fix
public class ReportWorker(IServiceScopeFactory scopeFactory, ILogger<ReportWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
for (var iteration = 1; iteration <= 2; iteration++)
{
using var scope = scopeFactory.CreateScope();
var reports = scope.ServiceProvider.GetRequiredService<IReportService>();
logger.LogInformation("Iteration {Iteration}: {Count} open reports (report service {InstanceId})",
iteration, reports.CountOpenReports(), reports.InstanceId);
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
}
}
}
IServiceScopeFactory is itself a singleton, so the validator accepts it in a
hosted service. Each iteration creates a scope, resolves a fresh IReportService
from it, and disposes the scope at the end of the iteration. In Development the app now starts,
and the two iterations show two instances:
info: FixLab.ReportWorker[0]
Iteration 1: 3 open reports (report service f4175b06-4a66-47d4-86b4-255b67ad2c69)
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: FixLab.ReportWorker[0]
Iteration 2: 3 open reports (report service 5bcafdf0-1e0d-4808-9d40-e2a793b2e59b)
If the service holds no per-scope state and is safe to share between threads, registering it
with AddSingleton also clears the error. Making the consumer scoped, the other fix
for an ordinary singleton, is not available here: as the descriptor in the error
shows, AddHostedService registers a singleton. A DbContext takes the
same scope-per-iteration pattern and should never become a singleton.
How it was reproduced
An empty app from dotnet new web -n FixLab -f net10.0 on .NET SDK 10.0.401 with
the ASP.NET Core 10.0.12 runtime, no packages added: IReportService registered
with AddScoped, and a ReportWorker derived from
BackgroundService, registered with AddHostedService, logging the
service's instance id on two iterations two seconds apart.
dotnet run --urls http://localhost:15510 uses the template's launch profile, which
sets Development, and failed at builder.Build(). One trap on the way to
Production: in SDK 10, -e and --environment are
dotnet run's own option for setting an environment variable, so
dotnet run --urls http://localhost:15510 --environment Production left the app in
Development and failed the same way;
dotnet run --urls http://localhost:15510 -e ASPNETCORE_ENVIRONMENT=Production
started it as Production. With ValidateOnBuild set to false and
ValidateScopes to true, the same message came from app.Run() instead.
Frequently asked
- Why does the error say singleton IHostedService when I never registered a singleton?
- AddHostedService registers your worker class as IHostedService with a singleton lifetime. The message names that service type, and the service descriptor in the same message names your class, so look at the constructors of your hosted services.
- How do I use a DbContext in a BackgroundService?
- Inject IServiceScopeFactory, create a scope for each unit of work, resolve the DbContext from scope.ServiceProvider, and dispose the scope when the work is done. Never keep the DbContext or the scope in a field of the worker.
- Why does my BackgroundService fail in Development but run in Production?
- Development validates service lifetimes when the app is built; Production does not. The bug is still there in Production: the worker keeps the first scoped instance for the life of the app, which the lab showed as the same instance id on every iteration.
More decoded errors in the Fixes category. The same error
from an ordinary AddSingleton class is covered in
Cannot consume scoped service from singleton.