我们在进行系统开发的时候,有的代码是需要运行在后台的。
比如服务器启动的时候在后台预先加载数据到缓存、每天凌晨3点把数据导出到备份数据库、每隔5s在两张表之间同步一次数据,这些代码不是运行在前台的。
ASP.NET Core中提供了托管服务(hosted service)来供我们编写运行在后台的代码。
1. 托管服务的基本使用
托管服务的使用非常简单,只要编写一个实现了IHostedService接口的类即可。
一般情况下我们编写从BackgroudService类继承的类,因为BackgroundService实现了IHostedService接口,并且帮我们处理了任务的取消等逻辑。
我们只要实现BackgroundService类中定义的抽象方法ExecuteAsync,在ExecuteAsync方法中编写后台执行的代码即可。
BackgroundService类实现了IDisposable接口,我们可以把任务结束后的清理代码写到Dispose方法中。
下面来编写一个简单的托管服务,如以下代码所示:
public class BackService(ILogger<BackService> _log) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//先延迟5秒
await Task.Delay(5000);
//从"D:/1.txt"读取内容
string abs = await File.ReadAllTextAsync("D:/1.txt");
//再延迟20秒
await Task.Delay(20000);
//将读到的内容输入到日志
_log.LogInformation(abs);
}
}
ExecuteAsync方法执行结束后,这个托管服务也就执行结束了,因此我们这里编写的托管服务不会常驻后台。
为了让托管服务能够运行,我们需要在Program.cs中调用AddHostedService方法把它注册到依赖注入容器中,如以下代码:
builder.Services.AddHostedService<BackService>();
托管服务会随着应用程序启动,当然托管服务是在后台运行的,不会阻塞ASP.NET Core中其他程序的运行。
执行上面的程序,控制台的输出结果:
![](https://ichiblog.cn/wp-content/uploads/2024/10/图片-14-1024x525.png)
在托管服务的执行过程中,网站中其他功能是可以正常使用的,这就证明托管服务确实是运行在后台的。
我们把”D:/1.txt”文件删除,再运行程序,由于文件不存在,当程序运行时,就会抛出异常。
从.NET6开始,当托管服务中发生中未处理异常的时候,程序就会自动停止并退出。
我们可以把HostOptions.BackgroundServiceExceptionBehavior设置为Ignore,这样当托管服务中发生未处理异常的时候,程序会忽略这个异常,而不是停止程序。
为了避免托管服务中出现未处理异常导致程序退出,我们要先做好预防工作,在代码中尽量避免异常的产生。
2. 托管服务中使用依赖注入的陷阱
托管服务是以单例的生命周期注册到依赖注入容器中的。
按照依赖注入容器的要求,长生命周期的服务不能依赖短生命周期的服务。
因此我们可以在托管服务中通过构造方法注入其他生命周期为单例的服务,但是不能注入生命周期为范围或者瞬态的服务。
由于日志系统的服务的生命周期为单例,因此我们在代码中可以通过构造方法注入ILogger服务。
但是我们通过构造方法直接注入EFCore Context的话,程序就会抛出异常。
因为通过AddDbContext注册的服务的生命周期是范围的。
我们可以通过构造方法注入IServiceScopeFactory服务,它可以用来创建IServiceScope对象。
这样我们就可以通过IServiceScope来创建短生命周期的服务了。
3. 案例:数据的定时导出
除了那些执行完任务就退出的托管服务之外,我们还可能需要编写常驻后台的托管服务,比如监控消息队列,当有数据进入消息队列就处理。
再如每隔10s把A数据库中的数据同步到B数据库中。
因为BackgroundService的ExecuteAsync代码执行结束后托管服务就退出了。
所以常驻后台的托管服务并不需要特殊的技术,我们只要让ExecuteAsync中的代码一直执行即可。
下面来实现一个常驻后台的托管服务,它实现的功能是每隔5S对数据库中的数据进行汇总,然后把汇总结果写入一个文本文件,如以下代码:
public class ExplortStaticBgService : BackgroundService
{
private readonly MySQLContext _context;
private readonly ILogger<ExplortStaticBgService> _logger;
private readonly IServiceScope serviceScope;
//注入IServiceScopeFactory服务
public ExplortStaticBgService(IServiceScopeFactory serviceScopeFactory)
{
//CreateScope
this.serviceScope = serviceScopeFactory.CreateScope();
var sp = serviceScope.ServiceProvider;
this._context = sp.GetRequiredService<MySQLContext>();
this._logger = sp.GetRequiredService<ILogger<ExplortStaticBgService>>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//是否已经请求了取消
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoExecuteAsync();
//每次执行间隔5S
await Task.Delay(5000);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户统计数据失败");
await Task.Delay(1000);//异常发生后暂停1s
}
}
}
private async Task DoExecuteAsync()
{
var items = await _context.Set<Songs>().FirstOrDefaultAsync(q => q.SongInitPlays == 19834);
StringBuilder sb = new();
sb.AppendLine($"Date:{DateTime.Now}");
sb.Append(items);
await File.WriteAllTextAsync("D:/1.txt",sb.ToString());
_logger.LogInformation("导出完成");
}
public override void Dispose()
{
base.Dispose();
serviceScope.Dispose();
}
}
在这个例子中,我们实现了”每隔5min做一次统计”的功能。
如果想要实现更复杂的定义任务,比如”每天凌晨3点执行数据备份任务”或者”每月初执行一个报表统计任务”等,那么我们可以使用Hangfire、Quartz.NET等更专业的定时任务开源项目。
4. Hangfire
Hangfire的中文文档:https://www.bookstack.cn/read/Hangfire-zh-official/README.md