一. 托管服务
1. 简介
使用背景:代码运行在后台。比如服务器启动的时候在后台预先加载数据到缓存,再比如定时任务凌晨1点需要遍历数据库修改状态等等。
注意:
常驻后台的托管服务并不需要特殊的技术,我们只要while (!stoppingToken.IsCancellationRequested) 让ExecuteAsync中的代码一直执行不结束就行了, 但是不能部署在IIS上。
因为如果挂在IIS上,闲置超时20分钟,是指20分钟内没有任何请求进行访问,如果有请求则这个闲置超时时间会重新计算。如果场景是定时任务,且期间没有请求,该方案不适合,
因为IIS会回收它,这一点类似Quartz.Net 部署在IIS上是一个道理的(可以用控制台方案解决 或 其它部署方案解决)。
2.核心说明
(1). 托管服务实现IHostedService接口,但我们通常用BackgroundService这个基类来做
3. 托管服务的异常处理
(1).从.NET 6开始,当托管服务中发生未处理异常的时候,程序就会自动停止并退出(之前程序不会停止)。可以把HostOptions.BackgroundServiceExceptionBehavior设置为Ignore,程序会忽略异常,而不是停止程序。 不过推荐采用默认的设置,因为“异常应该被妥善的处理,而不是被忽略”。
(2).通常建议在ExecuteAsync方法中把代码用try-catch包裹起来,当发生异常的时候,记录日志中或发警报等。
(3). 代码实操:
A. 后台常驻服务,通过 while (!stoppingToken.IsCancellationRequested) 来判断
B. 业务代码要try-catch包裹
C. 通过 await base.StopAsync(stoppingToken); 停止后台服务
D. 服务注册 builder.Services.AddHostedService<TestBackService1>(); 【这是单例模式的注入】
后台服务代码
public class TestBackService1 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------后台任务开启-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//模拟实际业务,输出当前时间
Console.WriteLine($"当前时间为:{DateTime.Now}");
//等待5s
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
//测试报错
//int.Parse("fdgdfg");
}
catch (Exception ex)
{
Console.WriteLine($"出错了:{ex.Message}");
//根据实际情况决定是否停止后台任务
await base.StopAsync(stoppingToken);
}
}
}
}
注册服务
//注册后台服务
builder.Services.AddHostedService<TestBackService1>();
运行结果
4. 托管服务中的DI
(1). 托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入请求内单例或者瞬时的服务。比如注入EF Core的上下文的话(默认是请求内单例的),程序就会抛出异常。
(2). 解决方案
创建一个IServiceScope对象,这样我们就可以通过IServiceScope来创建所需声明周期的服务即可
这里通常有两种写法,
要么直接在ExecuteAsync中的using中构建出来所需声明周期的服务 【详见代码版本2】
要么在构造函数中创建出来所需声明周期的服务(需要dispose一下) 【详见代码版本3】
(3). 代码实操
A.新建EF上下午MyDBContext, 然后 builder.Services.AddScoped<MyDbContext>(); 请求内单例的
B.在后台服务中TestBackServiceDI中注入MyDBContext。 运行:直接报错,错误内容如下图,大体意思:不同生命周期的内容不能相互注入使用 【详见版本1代码】
代码分享:
public class TestBackServiceDI : BackgroundService
{
#region 版本1--构造函数注入MyDbContext【报错】
private readonly MyDbContext dbContext;
public TestBackServiceDI(MyDbContext dbContext)
{
this.dbContext = dbContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------后台任务开启-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//调用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"出错了:{ex.Message}");
//根据实际情况决定是否停止后台任务
await base.StopAsync(stoppingToken);
}
}
}
#endregion
}
报错:
C.注入IServiceProvider service
D.在ExecuteAsync中通过using+ serviceScope.ServiceProvider.GetRequiredService<MyDbContext>(); 创建dbContext即可,运行代码,直接输出Hello world 【详见代码版本2】
E.详见代码版本3
解决方案1代码 【推荐】
public class TestBackServiceDI : BackgroundService
{
#region 版本2-通过service.CreateScope()创建
private readonly IServiceProvider service;
public TestBackServiceDI(IServiceProvider service)
{
this.service = service;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------后台任务开启-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using (IServiceScope serviceScope = service.CreateScope())
{
var dbContext = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
//调用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (Exception ex)
{
Console.WriteLine($"出错了:{ex.Message}");
//根据实际情况决定是否停止后台任务
await base.StopAsync(stoppingToken);
}
}
}
#endregion
}
解决方案2代码
public class TestBackServiceDI : BackgroundService
{
#region 版本3-通过构造函数中创建符合生命期的EF上下文
private readonly IServiceScope serviceScope;
private readonly MyDbContext dbContext;
public TestBackServiceDI(IServiceProvider service)
{
this.serviceScope = service.CreateScope();
this.dbContext = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("----------后台任务开启-------------------");
while (!stoppingToken.IsCancellationRequested)
{
try
{
//调用EF上下午
string result = dbContext.GetMsg();
Console.WriteLine(result);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (Exception ex)
{
Console.WriteLine($"出错了:{ex.Message}");
//根据实际情况决定是否停止后台任务
await base.StopAsync(stoppingToken);
}
}
}
public override void Dispose()
{
base.Dispose();
serviceScope.Dispose();
}
#endregion
}
二. 内置数据校验
三. FluentValidition
四. 程序发布部署
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。