Inject a DbContext Instance Into BackgroundService in .NET Core

What is DbContext?

DbContext serves as a bridge between the application and the database, providing a high-level abstraction for database operations and simplifying the development process.

What is BackgroundService in .net?

  1. BackgroundService is a base class provided by Microsoft.Extensions.Hosting namespace, primarily used for implementing long-running background tasks or services in applications built on the .NET Core or ASP.NET Core platforms.
  2. BackgroundService simplifies the implementation of background tasks in .NET Core and ASP.NET Core applications, providing a structured way to run long-running processes efficiently and integrate them with the application's lifecycle.

Why We Cannot Inject DbContext Into a BackgroundService Directly?

  1. The primary reason we can't directly inject a DbContext instance into a BackgroundService is due to the limitations imposed by the dependency injection lifetimes available for injection in .NET Core and ASP.NET Core.
  2. In ASP.NET Core applications, dependency injection supports three lifetimes: Singleton, Scoped, and Transient. However, BackgroundService instances are essentially transient, meaning they are created and disposed of on demand. On the other hand, Entity Framework Core's DbContext instances are typically registered as scoped services. Scoped services have a lifetime tied to the current request in ASP.NET Core applications.
  3. Because of this mismatch in lifetimes, attempting to directly inject a DbContext into a background service would result in a runtime error, as the service provider wouldn't be able to resolve the dependency correctly.
  4. To work around this limitation, one common approach is to manually create a scope within the BackgroundService implementation when accessing the DbContext. This allows you to obtain an instance of the DbContext within the scope of the hosted service's execution. However, it's important to manage the scope properly to avoid potential issues such as memory leaks or database connection leaks.

Why DbContext instances is scoped lifetime?

  1. The reason DbContext instances typically have a scoped lifetime in ASP.NET Core applications is closely tied to the Unit of Work pattern and the need to ensure transactional integrity and isolation of database operations.
  2. In the Unit of Work pattern, multiple database operations are often grouped together to form a logical unit of work. This unit of work may involve multiple read-and-write operations, which should either succeed together or fail together. By using a scoped lifetime for DbContext, ASP.NET Core ensures that the same instance of DbContext is used throughout the duration of a single request.
  3. Additionally, DbContext instances are not designed to be thread-safe and should not be shared across multiple threads. While Entity Framework typically detects concurrent usage attempts and throws an InvalidOperationException, there are scenarios where it might not catch such misuse, potentially resulting in unpredictable behavior and data corruption. Therefore, it's crucial to adhere to best practices and ensure that each thread operates on its dedicated DbContext instance to maintain data integrity and application stability.

How to Inject a DbContext Instance Into a BackgroundService Using IServiceScopeFactory?

We use the BackgroundService to run different background tasks. In our case, we’ll create a service that seeds our database with some WeatherForecast info.

public class WeatherForecastService : BackgroundService
{
    private readonly IServiceScopeFactory _service;


    public WeatherForecastService(IServiceScopeFactory scopeFactory) 
    {
        _service = scopeFactory;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {        
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    => Task.CompletedTask;
}

First, we create our WeatherForecastService class and implement the BackgroundService. Then, we implement the ExecuteAsync() and StopAsync() methods 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.

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
    var summaries = new[]
     {
     "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
     };

    using var scope = _service.CreateScope();
    using var context = scope.ServiceProvider.GetRequiredService<WeatherForecastContext>();

    context.Database.EnsureCreatedAsync(stoppingToken);

    context.WeatherForecasts.AddRange(Enumerable.Range(1, 10)
    .Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = summaries[Random.Shared.Next(summaries.Length)]
    }));

    context.SaveChangesAsync(stoppingToken);

    return Task.CompletedTask;
}

In the ExecuteAsync() 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 WeatherForecastContext class. This whole process will get a context instance from the DI container.

Note that we can also inject an IServiceProvider instance in our WeatherForecast seeding service and the code will work without any additional changes.

The distinction between the two (IServiceScopeFactory, IServiceProvider) interfaces lies in their lifetimes. While IServiceScopeFactory always maintains a Singleton lifetime, the lifetime of IServiceProvider depends on the class it's injected into. Additionally, IServiceProvider features a CreateScope() method, similar to IServiceScopeFactory, allowing for the resolution of IServiceScopeFactory through it. Directly utilizing IServiceScopeFactory streamlines the process, eliminating an extra step for the compiler.

Finally, we can register our service.

builder.Services.AddHostedService<WeatherForecastService>();

The above code sample is available with the name 'InjectDbContext_IServiceScopeFactory'.

How to Inject a DbContext Instance Into a BackgroundService Using IDbContextFactory?

In our Program class, we use the AddDbContext<TContext>() method to register our context to the DI container. But there is another way we can inject a DbContext instance.

builder.Services.AddDbContextFactory<WeatherForecastContext>(options
        => options.UseInMemoryDatabase("WeatherForecasts"));

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.

In scenarios, where the scope of the DbContext doesn't match that of the consuming service, such as in background services, the AddDbContextFactory<TContext>() method comes in handy. This method registers an IDbContextFactory<TContext> instance as a Singleton service in the dependency injection container. Conveniently, the compiler also registers the DbContext itself with a Scoped lifetime alongside the context factory. This setup ensures flexibility and proper scoping for both the factory and the DbContext instance.

Now we can proceed to update our hosted service’s constructor.

public class WeatherForecastBackGroundservice : BackgroundService
{
    private readonly IDbContextFactory<TContext> _contextFactory;

    public WeatherForecastBackGroundservice(IDbContextFactory<WeatherForecastContext> contextFactory) 
    {
        _contextFactory = contextFactory;
    }
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    => Task.CompletedTask;
}

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 ExecuteAsync() method.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    var summaries = new[]
    {
         "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    using var context = await _contextFactory.CreateDbContextAsync(stoppingToken);

    await context.Database.EnsureCreatedAsync(stoppingToken);

    context.WeatherForecasts.AddRange(Enumerable.Range(1, 20)
        .Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }));

    await context.SaveChangesAsync(stoppingToken);            
}

To access our context, we call the CreateDbContextAsync() method on the context factory we injected. This will initialize our WeatherForecastContext class and we can then use it to seed the database.

The above code sample is available with the name 'InjectDbContext_IDbContextFactory'.

When we run either of the above applications, we will get a swagger page loaded with 2 APIs.

Swagger page

is with default implementation by .net framework (/weather forecast)

Default implementation

Weather forecast

is with a new implementation to load the details from In-memory where the data is saved using BackGroundService (/weatherforecastFromIDbContextFactory).

BackGroundService

Summary

In this discussion, we explore two strategies for injecting a DbContext instance into a class implementing the Background service. Challenges arise from the Scoped lifetime of the database context and the constraints imposed by injection lifetimes within a hosted service. However, these hurdles can be effectively addressed by leveraging either an IServiceScopeFactory or an IDbContextFactory. These distinct factories offer flexibility in aligning the scope of the context with the needs of background services, ensuring meticulous data isolation and thread safety.


Similar Articles