2

I have a few background jobs in my application that modify the database on an interval (from a few minutes to several hours). All of them have a while loop that uses the stoppingToken.IsCancellationRequested condition.

Currently I am creating and disposing a scope inside the loop, which means on every iteration, a scope needs to be created and disposed. My question is that from a performance and security point of view where should I create my scope? Inside the loop for each iteration or outside the loop once in the application lifetime?

public class MyBackgroundJob : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public MyBackgroundJob(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Here? 
        //using var scope = _serviceProvider.CreateScope();
        //var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Or here? 
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            // Do some work

            try
            {
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (TaskCanceledException)
            {
                // application is shutting down
                // ignore this
            }
        }
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Parsa99
  • 307
  • 1
  • 13

2 Answers2

6

Most likely, the scope should be created inside the loop. It depends on Do Some Work and how DbContext is used. A better idea may be to use a DbContext Factory instead of a scope.


A DbContext isn't a connection. It doesn't even keep a connection open until it has to read or save data. Once it's done, it closes the connection. A DbContext is a Unit-of-Work that tracks all loaded objects , any changes made to them and persists all changes in a single transaction when you call SaveChanges. Disposing it also discards the changes, effectively rolling them back.

If the loop performs individual "transactions", the scope and DbContext must be created inside the loop. This is most likely the case here, as the loop repeats every 5 minutes.

There's no real benefit to a long-lived DbContext. To keep a long-lived DbContext you'd have to ensure that tracking is disabled and Find() isn't used, to prevent caching of loaded entities. In essence, EF Core would work like Dapper, to execute queries and map results to objects.

Using a DbContext Factory

If the scope is only used to create DbContext instances, an alternative would be to use a DbContextFactory. The factory will be used to create the DbContext inside the loop without explicitly creating a scope.

The factory is registered as a Singleton with AddDbContextFactory:

builder.Services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

The background service then uses it to create DbContexts:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyBackgroundJob(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using (var context = _contextFactory.CreateDbContext())
        {
            // Do Some work
        }
    }
    ...

Using a pooled DbContext Factory

In general, DbContext objects are lightweight but they do have some overhead. A long lived service will allocate (and dispose) many of them over its lifetime though. To eliminate even this overhead, a pooled DbContext Factory can be used to create reusable DbContext instances. When Dispose() is called, the DbContext instance is cleared and put into a context pool so it can be reused.

Only the factory registration needs to change, to AddDbContextPool :

builder.Services.AddDbContextPool<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

The service code remains the same.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
1

Inside the loop for each iteration or outside the loop once in the application lifetime?

My personal rule of thumb is to create a scope per "tick" (i.e. in this case - iteration of the endless loop), otherwise you can encounter different problems with resource/service lifetime being extended to the lifetime of the app (though in some apps it can be fine for example CLI tools). This especially can be evident when working EF Core context used somewhere down the pipeline, which can lead do different problems like stale data, performance degradation and memory-leaky behavior due to the change tracking.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132