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
}
}
我正在使用 .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
}
}