ASP.NET Core HostedService 正在尝试在数据库创建之前访问它

ASP.NET Core HostedService is trying to access database before it's created

我正在使用 .NET 5.0 开发一个 asp.net 项目;在这个项目中,我首先使用 Entity Framework 代码和 sql 精简版文件数据库。

startup.cs 文件中,我使用以下代码以编程方式创建和更新数据库模式:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
    // migrate any database changes on startup (includes initial db creation)
    dataContext.Database.Migrate();

    ...
}

然后我有一项服务可以用来清除某些 table 数据库中的旧数据。它会在启动时定期清理它们:

public class TimedDbCleanerService : IHostedService, IDisposable
{

    ...
}

具有这些功能:

public Task StartAsync(CancellationToken stoppingToken)
{
    _timer = new Timer(DoWork, null, TimeSpan.Zero,
        TimeSpan.FromHours(_dbCleanerSettings.Hours));

    return Task.CompletedTask;
}

private void DoWork(object state)
{
    // create scoped dbcontext
    using (var scope = _scopeFactory.CreateScope())
    {
        var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();

        // check and remove stuff

        dbContext.SaveChanges();
    }
}

startup.cs中用这行代码注册:

services.AddHostedService<TimedDbCleanerService>();

这里的问题是,如果在执行项目之前数据库文件不存在,清理服务会尝试访问 table 尚不存在的数据库。

使用调试器我可以看到 Database.Migrate() 在服务访问数据库之前被调用,但是迁移似乎是一个需要时间才能完成的异步任务。

在创建和启动清洁服务之前,是否有一种方法可以等待迁移完全执行其工作?

创建一个 class 来协调迁移。这将执行迁移并帮助其他人使用 SemaphoreSlim.

等待完成

在这个class里面我们注入DbContext和运行迁移,然后让等待迁移的线程继续执行。

public class DatabaseMigrator
{
    private readonly AppDbContext _dbContext;

    private static readonly SemaphoreSlim _migrationEvent = new SemaphoreSlim(0);

    public DatabaseMigrator(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Migrate()
    {
        _dbContext.Database.Migrate();
        _migrationEvent.Release(1);
    }

    public Task WaitForMigrationAsync(CancellationToken cancellationToken = default)
    {
        return _migrationEvent.WaitAsync(cancellationToken);
    }
}

将此 class 注册到 DI:

services.AddSingleton<DatabaseMigrator>();

在您的 Main 函数或 Startup.Configure 中,注入此函数并 运行 迁移:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseMigrator databaseMigrator)
{
    databaseMigrator.Migrate();
    // ...
}

在后台服务中,我们不注入迁移器,因为它依赖于范围内的服务,例如 DbContext。相反,我们从我们创建的范围中解析一个,等待迁移完成。

class CleanerService : BackgroundService
{
    private IServiceScopeFactory _serviceScopeFactory;

    public CleanerService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var migrator = scope.ServiceProvider.GetRequiredService<DatabaseMigrator>();
            await migrator.WaitForMigrationAsync(stoppingToken);
        }

        await PerformCleanup();
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            await PerformCleanup();
        }
    }

    private async Task PerformCleanup()
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // ... clean things up
    }
}

一个小问题:在异步上下文中使用 Timer 会强制您使用 void,这会在异步工作时带来一大堆问题,您可以调用 Task.Delay 在一个循环中,这会将线程释放到线程池中,让它在等待的同时执行其他任务:

await PerformCleanup();
while (!stoppingToken.IsCancellationRequested)
{
    await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
    await PerformCleanup();
}

更好的替代方法是在启动应用程序之前执行迁移,这会大大简化事情。线程之间不需要同步。

public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();

    using (var scope = host.Services.CreateScope())
    {
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        db.Database.Migrate();
    }
    
    // database migration is completed
    
    host.Run();
}

由于这是在后台服务启动之前完成的 运行,因此不存在竞争条件的风险。

现在后台服务更简单了:

class CleanerService : BackgroundService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public CleanerService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await PerformCleanup();
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            await PerformCleanup();
        }
    }

    private async Task PerformCleanup()
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // ... clean things up
    }
}