Cannot Consume Scoped Service From Singleton: .NET 10 Fixes

Last updated: November 2, 2026 · Tested with .NET 10.0.12 (SDK 10.0.401), EF Core 10.0.14 with SQLite, Windows 11

Short answer: this error means a singleton is asking for a service that should live for one request. A singleton lives for the whole app, so it would keep that one instance forever. To fix it, don’t inject the scoped service: inject IServiceScopeFactory and create a scope per operation, or use IDbContextFactory for a DbContext. Don’t switch off the check. In my tests the same bug ran without any error in Production, and then served stale data and failed 29 of 30 parallel requests.

Where it happens What you see Fix
Singleton class takes a scoped service Cannot consume scoped service 'X' from singleton 'Y' IServiceScopeFactory, or change a lifetime
BackgroundService takes a DbContext ... from singleton 'Microsoft.Extensions.Hosting.IHostedService' Create a scope inside ExecuteAsync
Singleton needs a DbContext Cannot consume scoped service 'ShopDb' IDbContextFactory
Singleton takes IOptionsSnapshot Cannot consume scoped service ...IOptionsSnapshot IOptionsMonitor
app.Services.GetRequiredService at startup Cannot resolve scoped service 'X' from root provider using var scope = app.Services.CreateScope()
Middleware constructor Same root provider error Move the parameter to InvokeAsync

The Error

The full message, thrown by builder.Build():

AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: PriceCache Lifetime: Singleton ImplementationType: PriceCache': Cannot consume scoped service 'IOrderRepository' from singleton 'PriceCache'.)

It comes from two ordinary registrations:

builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddSingleton<PriceCache>();
public class PriceCache(IOrderRepository repository)

A scoped service gets one instance per request. A singleton gets one instance per app. If the singleton holds a scoped service, that service stops being per-request and lives as long as the app. This is often called a captive dependency, and the error exists to stop it.

How I Tested It

One ASP.NET Core project with 21 scenarios, each with its own host. The ones that serve requests run on real Kestrel and are called over HTTP. I switched the environment between Development and Production with WebApplicationOptions. The database is SQLite through EF Core; the error comes from the DI container, so it isn’t specific to any database.

Why It Works in Production and Fails on Your Machine

Scope validation is on in Development and off everywhere else. Same code, same registrations, with the environment set to Production:

started
/singleton -> 200 repository #1
/singleton -> 200 repository #1
/scoped -> 200 repository #2
/scoped -> 200 repository #3

The app starts with no error. Requests through the singleton always get repository #1, while a normal scoped injection gets a new one per request. So “it works on the server” doesn’t mean the code is fine. The bug is still there, and nothing reports it.

With a DbContext it does real damage. I registered a singleton service that holds ShopDb and reads a price:

public class ProductService(ShopDb db)
{
    public decimal Price(int id) => db.Products.Find(id)!.Price;

Then I changed the price in the database from a separate scope and read it again:

/price -> 200 50.0
POST /price -> 200
/price -> 200 50.0

The database says 99, and the API keeps saying 50, because the context’s change tracker lives forever and Find answers from it. That’s stale data with a 200 response.

Under parallel load it’s worse. DbContext isn’t thread-safe, and now every request shares one. 30 parallel requests:

200: 1, 500: 29
InvalidOperationException: An attempt was made to use the context instance while it is being configured. A DbContext instance cannot be used inside 'OnConfiguring' since it is still being configured a...
InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instan...

If you see “A second operation was started” in production logs and can’t find two awaits on one context, look for a singleton holding your DbContext.

Catch It in Production Builds Too

You can turn the check on everywhere. With this, the Production build failed at startup with the same error as Development:

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

A crash at deploy time is much cheaper than stale prices for a week. Scope validation adds some work on service resolution, so if you’d rather not run it in production, run one startup test with these settings in CI.

Fix 1: Create a Scope Yourself

This is the general fix, and the right one for a BackgroundService. Hosted services are registered as singletons, which is why this common mistake:

public class BadWorker(ShopDb db) : BackgroundService

gives you a message that doesn’t mention your class as the singleton:

AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: BadWorker': Cannot consume scoped service 'ShopDb' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)

Inject IServiceScopeFactory, which is a singleton, and resolve inside a scope:

public class GoodWorker(IServiceScopeFactory scopes) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var scope = scopes.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<ShopDb>();
        Console.WriteLine($"worker sees {await db.Products.CountAsync(stoppingToken)} products");
worker sees 2000 products

In a worker that loops, create the scope inside the loop, one per unit of work. A scope created once at the top of ExecuteAsync is the same captive dependency with extra steps.

Two traps with scopes. Use CreateAsyncScope if anything in the scope might be async-disposable, because a plain using throws:

InvalidOperationException: 'AsyncOnly' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.

And always dispose the scope. I created 3 without disposing them. The services were never cleaned up, even after the app itself was disposed:

created 3, disposed 0
after app dispose: created 3, disposed 0

Fix 2: IDbContextFactory for DbContext

If the scoped service is just a DbContext, this is cleaner:

builder.Services.AddDbContextFactory<ShopDb>(o => o.UseSqlite("Data Source=shop.db;Pooling=False"));
builder.Services.AddSingleton<ReportService>();
public class ReportService(IDbContextFactory<ShopDb> factory)
{
    public async Task<int> CountAsync()
    {
        await using var db = await factory.CreateDbContextAsync();
        return await db.Products.CountAsync();
    }
}
/count -> 200 2000
/count -> 200 2000

Fix 3: Change a Lifetime

Often the simplest answer is that the class didn’t need to be a singleton. If PriceCache has no state of its own, make it scoped. If the repository holds no per-request state, it could be a singleton instead, but only if everything below it can be too. Don’t make a DbContext a singleton to silence the error: that gives you the stale prices and failed requests above, on purpose. For the related lifetime rules, see the C# dependency injection guide.

Cannot Resolve Scoped Service From Root Provider

This is the same problem with a different message. The app’s root provider has no scope, so in Development this fails, typically when running migrations or seeding at startup:

var r = app.Services.GetRequiredService<IOrderRepository>();
InvalidOperationException: Cannot resolve scoped service 'IOrderRepository' from root provider.

In Production it returned the same instance twice, so it was a singleton in practice:

same instance from root: True

Create a scope:

using (var scope = app.Services.CreateScope())
{
    var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
resolved: OrderRepository

Convention middleware has the same trap, because it’s built once. A scoped service in its constructor gives the same root provider error:

public class BadMiddleware(RequestDelegate next, IOrderRepository repository)

Move it to InvokeAsync, which is called per request and gets scoped services from the request:

public class GoodMiddleware(RequestDelegate next)
{
    public Task InvokeAsync(HttpContext context, IOrderRepository repository)

Three Cases That Fool You

IOptionsSnapshot. It’s scoped, so a singleton can’t take it:

AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: SnapshotReader Lifetime: Singleton ImplementationType: SnapshotReader': Cannot consume scoped service 'Microsoft.Extensions.Options.IOptionsSnapshot`1[ShopOptions]' from singleton 'SnapshotReader'.)

Use IOptionsMonitor and read CurrentValue:

public class MonitorReader(IOptionsMonitor<ShopOptions> options)
{
    public string Currency => options.CurrentValue.Currency;
}

A transient in between. Here OrderProcessor (singleton) takes OrderValidator (transient), which takes the scoped repository. The message names the transient as the scoped service, which sends you looking at the wrong registration. OrderValidator is AddTransient; the real scoped service is one level down:

AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: OrderProcessor Lifetime: Singleton ImplementationType: OrderProcessor': Cannot consume scoped service 'OrderValidator' from singleton 'OrderProcessor'.)

A factory lambda skips the startup check. The compiler and Build() can’t see inside the lambda, so this started fine in Development and failed on the first request:

builder.Services.AddSingleton(sp => new PriceCache(sp.GetRequiredService<IOrderRepository>()));
started
/ -> 500 System.InvalidOperationException: Cannot resolve scoped service 'IOrderRepository' from root provider.

Also note what the check doesn’t cover. A transient held by a singleton gives no error at all, and it effectively becomes a singleton:

/ -> 200 held 1, new 2
/ -> 200 held 1, new 3

That’s fine for a stateless helper, but it’s a real captive dependency if the transient holds per-request data.

Checklist

  • Read the second name in the message: that’s the singleton. If it says IHostedService, it’s a background worker.
  • Follow the chain if the “scoped” name is really a transient.
  • Stateless class? Change its lifetime.
  • DbContext in a singleton: IDbContextFactory.
  • Anything else: IServiceScopeFactory, with CreateAsyncScope, always disposed, one scope per unit of work.
  • Options: IOptionsMonitor.
  • Run ValidateScopes and ValidateOnBuild in a CI startup test, so Production can’t hide this.

A captured context can also cause EF Core’s “cannot be tracked” error, because the tracker is never cleared.

Related: dependency injection in C# · Hangfire vs Quartz.NET vs IHostedService · repository and unit of work · all ASP.NET Core tutorials

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *