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'.
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);
}
}
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.
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);
}
}
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 IServiceProvider
resolves an IServiceScopeFactory
instance and calls CreateScope()
on it:
public static IServiceScope CreateScope(this IServiceProvider provider)
{
return provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}
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 2 ways I can help you:
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,300+ students here.
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.