Using Scoped Services From Singletons in ASP.NET Core

Milan Jovanović - Feb 19 - - Dev Community

Did you ever need to inject a scoped service into a singleton service?

I often need to resolve a scoped service, like the EF Core DbContext, in a background service.

Another example is when you need to resolve a scoped service in ASP.NET Core middleware.

If you ever tried this, you were probably greeted with an exception similar to this one:

System.InvalidOperationException: Cannot consume scoped service 'Scoped' from singleton 'Singleton'.
Enter fullscreen mode Exit fullscreen mode

Today, I'll explain how you can solve this problem and safely use scoped services from within singletons in ASP.NET Core.

ASP.NET Core Service Lifetimes

ASP.NET Core has three service lifetimes:

  • Transient
  • Singleton
  • Scoped

Transient services are created each time they're requested from the service container.

Scoped services are created once within the scope's lifetime. For ASP.NET Core applications, a new scope is created for each request. This is how you can resolve scoped services within a given request.

ASP.NET Core applications also have a root IServiceProvider used to resolve singleton services.

So, what can we do if resolving a scoped service from a singleton throws an exception?

The Solution - IServiceScopeFactory

What if you want to resolve a scoped service inside a background service?

You can create a new scope (IServiceScope) with its own IServiceProvider instance. The scoped IServiceProvider can be used to resolve scoped services. When the scope is disposed, all disposable services created within that scope are also disposed.

Here's an example of using the IServiceScopeFactory to create a new IServiceScope. We're using the scope to resolve the ApplicationDbContext, which is a scoped service.

The BackgroundJob is registered as a singleton when calling AddHostedService<BackgroundJob>.

public class BackgroundJob(IServiceScopeFactory serviceScopeFactory)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using IServiceScope scope = serviceScopeFactory.CreateScope();

        var dbContext = scope
            .ServiceProvider
            .GetRequiredService<ApplicationDbContext>();

        // Do some background processing with the EF database context.
        await DoWorkAsync(dbContext);
    }
}
Enter fullscreen mode Exit fullscreen mode

Scoped Services in Middleware

What if you want to use a scoped service in ASP.NET Core middleware?

Middleware is constructed once per application lifetime.

If you try injecting a scoped service, you'll get an exception:

System.InvalidOperationException: Cannot resolve scoped service 'Scoped' from root provider.
Enter fullscreen mode Exit fullscreen mode

There are two ways to get around this.

First, you could use the previous approach with creating a new scope using IServiceScopeFactory. You'll be able to resolve scoped services. But, they won't share the same lifetime as the other scoped service in the same request. This could even be a problem depending on your requirements.

Is there a better way?

Middleware allows you to inject scoped services in the InvokeAsync method. The injected services will use the current request's scope, so they'll have the same lifetime as any other scoped service.

public class ConventionalMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(
        HttpContext httpContext,
        IMyScopedService scoped)
    {
        scoped.DoSomething();

        await _next(httpContext);
    }
}
Enter fullscreen mode Exit fullscreen mode

IServiceScopeFactory vs. IServiceProvider

You might see examples using the IServiceProvider to create a scope instead of the IServiceScopeFactory.

What's the difference between these two approaches?

The CreateScope method from IServiceProviderresolves an IServiceScopeFactory instance and calls CreateScope() on it:

public static IServiceScope CreateScope(this IServiceProvider provider)
{
    return provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}
Enter fullscreen mode Exit fullscreen mode

So, if you want to use the IServiceProvider directly to create a scope, that's fine.

However, the IServiceScopeFactory is a more direct way to achieve the desired result.

Summary

Understanding the difference between Transient, Scoped, and Singleton lifetimes is crucial for managing dependencies in ASP.NET Core applications.

The IServiceScopeFactory provides a solution when you need to resolve scoped services from singletons. It allows you to create a new scope, which you can use to resolve scoped services.

In middleware, we can inject scoped services into the InvokeAsync method. This also ensures the services use the current request's scope and lifecycle.

Thanks for reading, and I'll see you next week!


P.S. Whenever you're ready, there are 3 ways I can help you:

  1. Modular Monolith Architecture: This in-depth course will transform the way you build monolith systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. Join the wait list here.

  2. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 2,400+ students here.

  3. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 1,050+ engineers here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .