ASP.NET Core - 在后台作业中使用范围服务

Par*_*a99 2 c# dependency-injection .net-core asp.net-core .net-7.0

我的应用程序中有一些后台作业,它们会定期(从几分钟到几个小时)修改数据库。它们都有一个while使用stoppingToken.IsCancellationRequested条件的循环。

目前,我正在循环内创建和处置一个范围,这意味着在每次迭代中,都需要创建和处置一个范围。我的问题是,从性能和安全的角度来看,我应该在哪里创建我的范围?在应用程序生命周期中每次迭代都在循环内部还是在循环外部?

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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Pan*_*vos 6

最有可能的是,作用域应该在循环内创建。这取决于Do Some Work以及如何DbContext使用。更好的想法可能是使用 DbContext Factory 而不是范围。


DbContext 不是连接。在必须读取或保存数据之前,它甚至不会保持连接打开。完成后,它会关闭连接。DbContext 是一个工作单元,它跟踪所有加载的对象、对它们所做的任何更改,并在调用时将所有更改保留在单个事务中SaveChanges。处理它也会放弃更改,从而有效地回滚它们。

如果循环执行单独的“事务”,则必须在循环内创建作用域和 DbContext。这里很可能就是这种情况,因为循环每 5 分钟重复一次。

长期存在的 DbContext 没有真正的好处。要保持长期存在的 DbContext,您必须确保禁用且不Find()使用跟踪,以防止缓存加载的实体。本质上,EF Core 将像 Dapper 一样工作,执行查询并将结果映射到对象。

使用 DbContext 工厂

如果范围仅用于创建 DbContext 实例,则替代方法是使用DbContextFactory。工厂将用于在循环内创建 DbContext,而无需显式创建范围。

该工厂使用AddDbContextFactory注册为单例:

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

然后后台服务使用它来创建 DbContext:

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
        }
    }
    ...
Run Code Online (Sandbox Code Playgroud)

使用池化 DbContext Factory

一般来说,DbContext 对象是轻量级的,但它们确实有一些开销。然而,长期存在的服务将在其生命周期内分配(和处置)其中的许多。为了消除这种开销,可以使用池化 DbContext Factory 来创建可重用的 DbContext 实例。调用时Dispose(),DbContext 实例将被清除并放入上下文池中,以便可以重用。

只需要将工厂注册更改为AddDbContextPool

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

服务代码保持不变。