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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

You can find out more about the Unit of Work pattern in our article Understanding the Unit of Work Pattern in C#.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!