Skip to main content
SEMastery
ASP.NETintermediate

Using Scoped Services From Singletons in ASP.NET Core

Learn the safe way to use scoped services inside a singleton in ASP.NET Core using IServiceScopeFactory, with simple examples and clear diagrams.

12 min readUpdated September 23, 2025

Imagine a big sweet shop that stays open all day. The shop itself is open from morning to night, the same shop every single hour. That is like a singleton in ASP.NET Core: it is created once and lives for the whole life of your app.

Now think about a fresh paper plate that the shopkeeper gives to each customer. One plate per customer. When that customer leaves, the plate is thrown away. That is like a scoped service: it is made fresh for each request and thrown away when the request ends.

Here is the tricky part. What if the shop (always open) wants to use a fresh plate (one per customer)? The shop cannot keep just one plate forever, because plates are meant to be used by one customer and then thrown away. If the shop held on to a single plate for the whole day, that plate would get dirty, stale, and wrong.

This is exactly the puzzle we solve in this article. A singleton wants to use a scoped service. ASP.NET Core stops you from doing it the naive way, and for a good reason. Let me show you why, and the clean way to do it right.

A quick refresher on service lifetimes

In ASP.NET Core, when you register a service in the dependency injection (DI) container, you pick how long it should live. There are three choices.

LifetimeHow long it livesA simple picture
SingletonOne instance for the whole appThe sweet shop, open all day
ScopedOne instance per requestA fresh paper plate per customer
TransientA new instance every time you askA new tissue each time you sneeze

You register them like this.

var builder = WebApplication.CreateBuilder(args);
 
// One for the whole app
builder.Services.AddSingleton<ICacheService, CacheService>();
 
// One per HTTP request
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
 
// A brand new one every time
builder.Services.AddTransient<IEmailSender, EmailSender>();
 
var app = builder.Build();

The diagram below shows how these three lifetimes behave over time as requests come in.

How the three lifetimes behave as requests arrive

The key idea: a singleton outlives every request. A scoped service is born and dies inside one request. That mismatch is the heart of our problem.

The error you will hit

Let's say you write a singleton that directly asks for a scoped service in its constructor.

// ICacheService is a SINGLETON
public class CacheService : ICacheService
{
    // Trying to inject a SCOPED service directly
    public CacheService(IOrderRepository orders)
    {
        // ...
    }
}

When you start the app, ASP.NET Core checks your service graph and throws an error that looks like this:

System.AggregateException: Some services are not able to be constructed
 ---> System.InvalidOperationException: Cannot consume scoped service
 'IOrderRepository' from singleton 'ICacheService'.

You may also see a close cousin of this message:

Cannot resolve scoped service 'IOrderRepository' from root provider.

Both messages point at the same rule. A long-lived thing must not hold on to a short-lived thing.

Why the framework blocks it

Singleton
Holds scoped
Stale object

Steps

1

Singleton

Created once, lives forever

2

Holds scoped

Grabs one scoped object

3

Stale object

Scoped object outlives its scope

The captive dependency problem in three steps

Why this rule exists (the captive dependency)

People sometimes feel annoyed by this error. But it is protecting you from a nasty bug called a captive dependency.

Think back to the sweet shop. If the shop kept one paper plate forever and served every customer on it, that plate would never get cleaned. It would carry crumbs from the first customer to the last. That is wrong and unsafe.

In code terms, a scoped service often holds things tied to one request:

  • A database context (DbContext) that tracks changes for that one request.
  • The current user's information.
  • A unit-of-work that must commit and then be thrown away.

If a singleton captured that scoped object, then:

  1. The scoped object would never be disposed at the end of a request.
  2. Every request would share the same stale object.
  3. A DbContext is not safe to use from many requests at once, so you could get crashes or wrong data.

The framework spots this danger early, at startup, instead of letting it blow up in production. That is a gift, not a punishment.

The danger when a singleton captures a scoped object

The correct fix: create a scope yourself

The clean answer is simple. Do not hold the scoped service. Instead, ask for a fresh scope each time you actually need it. When you are done, throw the scope away. This matches what ASP.NET Core does for each HTTP request behind the scenes.

To do this, you inject one of two things into your singleton:

  • IServiceScopeFactory (the recommended choice), or
  • IServiceProvider (also fine).

Both of these are themselves registered as singletons, so a singleton is allowed to hold them. That is the trick that makes everything safe.

Here is the recommended pattern using IServiceScopeFactory.

public class CacheService : ICacheService
{
    private readonly IServiceScopeFactory _scopeFactory;
 
    public CacheService(IServiceScopeFactory scopeFactory)
    {
        // Allowed: IServiceScopeFactory is a singleton
        _scopeFactory = scopeFactory;
    }
 
    public async Task RefreshOrdersAsync()
    {
        // 1. Make a fresh scope (like a new paper plate)
        using IServiceScope scope = _scopeFactory.CreateScope();
 
        // 2. Resolve the scoped service INSIDE the scope
        var orders = scope.ServiceProvider
            .GetRequiredService<IOrderRepository>();
 
        // 3. Use it
        var all = await orders.GetAllAsync();
 
        // 4. The 'using' disposes the scope here,
        //    which disposes the scoped service too
    }
}

Notice the three rules:

  • Create the scope only when you need it, not in the constructor.
  • Resolve scoped services from scope.ServiceProvider, not from the root.
  • Wrap the scope in a using block so it is disposed cleanly.

The safe scope pattern

Create scope
Resolve
Use
Dispose

Steps

1

Create scope

CreateScope() makes a fresh box

2

Resolve

Get scoped service from the box

3

Use

Do the work

4

Dispose

using block cleans up

What happens each time the singleton needs a scoped service

How the scope flows through the container

It helps to see the picture. The DI container has a root provider that lives for the whole app. Each scope is a small child of that root. Scoped services live inside a scope and die with it.

The root provider and the scopes it creates

When your singleton calls CreateScope(), it makes a small temporary world (scope A, scope B, and so on). Inside that world, scoped services are created fresh. When you dispose the scope, that little world is cleaned up, and the scoped services go with it. No stale crumbs left behind.

The IServiceProvider version

Some teams prefer to inject IServiceProvider instead. This works just as well, because IServiceProvider.CreateScope() quietly uses IServiceScopeFactory under the hood. Here is the same idea.

public class CacheService : ICacheService
{
    private readonly IServiceProvider _provider;
 
    public CacheService(IServiceProvider provider)
    {
        _provider = provider;
    }
 
    public async Task RefreshOrdersAsync()
    {
        using IServiceScope scope = _provider.CreateScope();
 
        var orders = scope.ServiceProvider
            .GetRequiredService<IOrderRepository>();
 
        await orders.GetAllAsync();
    }
}

Which one should you pick? The table below compares them.

ChoiceGood forNote
IServiceScopeFactoryClear intent, easy to testRecommended by the docs
IServiceProviderWhen you already have it aroundUses the factory internally anyway

Both are correct. IServiceScopeFactory is a little clearer because its name says exactly what it does: it makes scopes.

A very common place this happens: background services

The most common spot where you meet this puzzle is a BackgroundService. A hosted background service is registered as a singleton. But it often needs scoped things like a DbContext to do its work. So you use the same scope pattern.

public class OrderCleanupService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
 
    public OrderCleanupService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Fresh scope for THIS round of work
            using (IServiceScope scope = _scopeFactory.CreateScope())
            {
                var db = scope.ServiceProvider
                    .GetRequiredService<AppDbContext>();
 
                await db.RemoveOldOrdersAsync(stoppingToken);
            }
 
            // Wait, then loop again with a brand new scope next time
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Notice that we create a new scope on every loop. We do not reuse one scope for the whole lifetime of the service. Each round of work gets its own clean DbContext, just like each customer gets a clean plate. This is the official guidance from Microsoft Learn for using scoped services inside a BackgroundService.

A background service making a fresh scope each loop

What about middleware?

Middleware is another place where lifetimes confuse people. A middleware class is created once (it behaves like a singleton). So you must not inject scoped services into its constructor.

But middleware has a nice trick. You can ask for scoped services as parameters of the InvokeAsync method. ASP.NET Core fills them in from the current request's scope, so they are perfectly safe there.

public class AuditMiddleware
{
    private readonly RequestDelegate _next;
 
    public AuditMiddleware(RequestDelegate next)
    {
        _next = next; // constructor: no scoped services here
    }
 
    // Scoped services are SAFE as method parameters
    public async Task InvokeAsync(HttpContext context, IOrderRepository orders)
    {
        await orders.LogAccessAsync(context.Request.Path);
        await _next(context);
    }
}

So for middleware, you usually do not need CreateScope() at all. You let the request parameters give you the scoped service. You only reach for IServiceScopeFactory when there is no request scope around, such as in a background job or a true singleton.

Common mistakes to avoid

Here are the traps that catch many developers. Keep this list close.

  • Creating the scope in the constructor. If you build a scope once and store it, you are back to the captive dependency problem. Always create the scope at the moment of use.
  • Forgetting to dispose. If you skip the using block, scoped services and their DbContext objects pile up and leak memory. Always dispose the scope.
  • Resolving from the root provider. Calling app.Services.GetRequiredService<IScopedThing>() on the root throws or gives a bad instance. Resolve from scope.ServiceProvider.
  • Sharing one scope across threads. A scope (and its DbContext) is not safe for many threads at once. Give each unit of work its own scope.

Decision: do I need CreateScope()?

Is there a request scope?
Use method params
Use CreateScope

Steps

1

Is there a request scope?

Controller or middleware InvokeAsync

2

Use method params

Let DI inject scoped service

3

Use CreateScope

Singleton or background job

A simple way to decide

A note on validation and safety

ASP.NET Core checks these lifetime rules at startup in the Development environment by default. This is called scope validation. It is why you find the error early instead of in production. You can keep this on, and you should. It catches captive dependencies and scoped-from-root mistakes before your users ever see them.

If you ever build your own provider, you can turn validation on explicitly.

var provider = services.BuildServiceProvider(new ServiceProviderOptions
{
    ValidateScopes = true,    // catch scoped-from-root
    ValidateOnBuild = true    // catch problems at startup
});

Keep both of these true while you develop. They are your safety net.

Putting it together

The whole idea fits in one sentence: a singleton may hold a scope factory, but never a scoped service. When the singleton needs scoped work done, it borrows a fresh scope, does the work, and gives the scope back.

This pattern works the same in .NET 10 (the current LTS release) as it has for years, so what you learn here will stay useful for a long time. The DI rules are stable and unlikely to change.

Quick recap

  • A singleton lives for the whole app; a scoped service lives for one request only.
  • Injecting a scoped service straight into a singleton throws "Cannot consume scoped service from singleton".
  • This rule blocks a captive dependency, where a stale scoped object would be shared by everyone.
  • The fix: inject IServiceScopeFactory (or IServiceProvider) and call CreateScope() when you need scoped work.
  • Resolve scoped services from scope.ServiceProvider, use them, then dispose the scope with a using block.
  • In a BackgroundService, make a fresh scope each loop, never one scope for the whole service.
  • In middleware, ask for scoped services as InvokeAsync parameters instead of CreateScope().
  • Keep ValidateScopes and ValidateOnBuild on so mistakes are caught at startup.

References and further reading

Related Posts