In this article, we’ll take a look at how to inject a DbContext instance into an IHostedService. We’ll also point out some important concepts we should be aware of.
Let’s start!
Why We Cannot Inject DbContext Into an IHostedService Directly
The main reason we cannot inject a DbContext
instance into an IHostedService
one is the fact that there are limitations on what can be injected into an IHostedService
instance. The two dependency injection lifetimes we can inject are Singleton and Transient. This makes it impossible for us to inject a DbContext
directly as the AddDbContext<TContext>()
method registers our context as a Scoped service.
But why do DbContext
instances have a Scoped lifetime? This is closely related to the Unit of Work pattern. According to it, there are cases where we must process several database operations together or not at all. With a Scoped lifetime we ensure that the same instance of a DbContext
is used when processing operations for a given request. This also ensures that database operations from different requests run in isolation and don’t interfere with one another.
Moreover, DbContext
instances are not thread-safe and should never be shared between threads. Entity Framework usually detects attempts to use DbContext
instance concurrently and will throw an exception of type InvalidOperationException
. In some cases it can miss concurrent use attempts which can lead to unexpected behavior and data corruption.
How to Inject a DbContext Instance Into an IHostedService Using IServiceScopeFactory
We use the IHostedService
interface to run different background tasks. In our case, we’ll create a service that seeds our database with some random cats:
public class CatsSeedingService(IServiceScopeFactory scopeFactory) : IHostedService { private static readonly int _maxAge = 15; private static readonly string[] _names = ["Whiskers", "Luna", "Simba", "Bella", "Oliver", "Shadow", "Gizmo", "Cleo", "Jasper", "Mocha"]; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
First, we create our CatsSeedingService
class and implement the IHostedService
interface. Next, we create two static fields – one representing the maximum age of a cat and another holding a list of names. Then, we implement the StopAsync()
method and return a completed task.
The key here is that we also have an IServiceScopeFactory
instance as a constructor parameter.
So, let’s use it and create a method to seed the data:
public async Task StartAsync(CancellationToken cancellationToken) { using var scope = scopeFactory.CreateScope(); using var context = scope.ServiceProvider.GetRequiredService<CatsDbContext>(); await context.Database.EnsureCreatedAsync(cancellationToken); context.Cats.AddRange(Enumerable.Range(1, 50) .Select(_ => new Cat { Id = Guid.NewGuid(), Name = _names[Random.Shared.Next(_names.Length)], Age = Random.Shared.Next(1, _maxAge) })); await context.SaveChangesAsync(cancellationToken); }
In the StartAsync()
method, we start by calling the CreateScope()
method on our scope factory to get an IServiceScope
instance. Then we use this scope to access its ServiceProvider
property and then call its GetRequiredService<T>()
method. The T
in this case is our CatsDbContext
class. This whole process will get a context instance from the DI container.
Note that we can also inject an IServiceProvider
instance in our cat seeding service and the code will work without any additional changes.
There is a subtle difference between the two interfaces – the IServiceScopeFactory
will always have a Singleton lifetime where the IServiceProvider
‘s lifetime will reflect the lifetime of the class it’s injected into. We also have a version of the CreateScope()
method on IServiceProvider
that will resolve the IServiceScopeFactory
on our behalf on calling its CreateScope()
method. Using the IServiceScopeFactory
directly saves the compiler an extra step.
Then, we seed our database with 50 random cats.
Finally, we can register our service:
builder.Services.AddHostedService<CatsSeedingService>();
How to Inject a DbContext Instance Into an IHostedService Using IDbContextFactory
In our Program
class, we use the AddDbContext<TContext>()
to method to register our context to the DI container. But there is another way we can inject an DbContext
instance:
builder.Services.AddDbContextFactory<CatsDbContext>(options => options.UseInMemoryDatabase("Cats"));
We start by changing the AddDbContext<TContext>()
method to an AddDbContextFactory<TContext>()
one. This will register a factory that we can use to create DbContext
instances.
We can use the AddDbContextFactory<TContext>()
method in cases where the scope of the DbContext
doesn’t align with the scope of the service that needs to consume it. Such cases are any background services or Blazor applications. This will add an IDbContextFactory<TContext>
instance to the DI container as a Singleton
service. For our convenience, the compiler will register the context itself with a Scoped lifetime alongside the context factory.
Now we can proceed to update our hosted service’s constructor:
public class CatsSeedingService(IDbContextFactory<CatsDbContext> contextFactory) : IHostedService { // The rest of the class is removed for brevity }
Here, we change the IServiceScopeFactory
parameter to an IDbContextFactory<TContext>
one. The dependency injection will be successful as the IDbContextFactory<TContext>
is registered with Singleton lifetime.
We need one final change in our StartAsync()
method:
public async Task StartAsync(CancellationToken cancellationToken) { using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await context.Database.EnsureCreatedAsync(cancellationToken); for (int i = 0; i < 50; i++) { context.Cats.Add(new() { Id = Guid.NewGuid(), Name = _names[Random.Shared.Next(_names.Length)], Age = Random.Shared.Next(1, _maxAge) }); } await context.SaveChangesAsync(cancellationToken); }
To access our context, we call the CreateDbContextAsync()
method on the context factory we injected. This will initialize our CatsDbContext
class and we can then use it to seed the database.
Conclusion
In this article, we examine two approaches to injecting a DbContext instance into a class that implements the IHostedService interface. The challenges are due to the Scoped lifetime of the database context and the limitations of injection lifetimes in a hosted service. But we can easily overcome those challenges by either using an IServiceScopeFactory or an IDbContextFactory one. These two different factories provide flexibility in aligning the context’s scope with the requirements of background services and help ensure proper data isolation and thread safety.