托管(轮询)服务

  1. 场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据在缓存,每天凌晨3点把数据导出到备份数据库,每隔5秒钟在两张表之间同步一次数据。
  2. 托管服务实现IHostdService接口,一般编写从BackgroundService继承的类。
    测试:延迟若干秒再读取文件,再延迟,再输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DemoBgService:BackgroundService
{
private readonly ILogger<DemoBgService> _logger; //注入日志服务

public DemoBgService(ILogger<DemoBgService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(3000); //等待3秒
string s = await File.ReadAllTextAsync("D:/SqlServer.txt"); //读取文件
await Task.Delay(3000); //等待3秒
_logger.LogInformation(s); // 输出日志
}
}

调用AddHostedService到依赖注入容器中

1
builder.Services.AddHostedService<DemoBgService>(); //注册后台服务

托管服务的异常问题

  1. 从.NET 6开始,当托管服务发生未处理异常的时候,程序就会自动停止并退出。可以把HostOptions.BackgroundServiceExceptionBehavior设置为lgnore,程序会忽略异常,而不是停止程序。不过推荐采用默认的设置,因为“异常应该被妥善的处理,而不是被忽略”。
  2. 要在ExecuteAsync方法中把代码用try…catch包裹起来,当发生异常的时候,记录日志中或发生报警等。
1
2
3
4
5
6
7
8
9
10
11
try
{
await Task.Delay(3000); //等待3秒
string s = await File.ReadAllTextAsync("D:/SqlServer.txt"); //读取文件
await Task.Delay(3000); //等待3秒
_logger.LogInformation(s); // 输出日志
}
catch (Exception ex)
{
Console.WriteLine("程序中出现异常"+ex); //输出异常信息
}

托管服务中使用依赖注入的陷阱

  1. 托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入生命周期为范围或者瞬太的服务。比如注入EF Core的上下文的话,程序就会抛出异常。

  2. 可以通过构造方法注入一个IServiceScopeFactory服务,它可以用来创建一个IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周全的服务了。记得再Dispose中释放IServiceScope。

  3. 下面实现一个常驻后台的托管服务,它实现的功能是每隔5s对数据库中的数据进行汇总,然后把汇总结果写入一个文本文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ExplortStatisticBgService : BackgroundService
{
private readonly MyDbContext ctx; //注入数据库上下文
private readonly ILogger<ExplortStatisticBgService> _logger; //注入日志服务
private readonly IServiceScope serviceScope; //注入服务范围
public ExplortStatisticBgService(IServiceScopeFactory scopeFactory)
{
this.serviceScope = scopeFactory.CreateScope(); //创建服务范围
var sp = serviceScope.ServiceProvider; //获取服务提供者
this.ctx = sp.GetRequiredService<MyDbContext>(); //获取数据库上下文
this._logger = sp.GetRequiredService<ILogger<ExplortStatisticBgService>>(); //获取日志服务
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!stoppingToken.IsCancellationRequested) // 判断是否取消请求
{
try
{
await DoExecuteAsync(); //执行具体操作
await Task.Delay(3000); //等待3秒
}
catch (Exception ex)
{
_logger.LogError(ex, "程序中出现异常"); //输出异常信息
await Task.Delay(3000); //等待3秒
}
}
}
private async Task DoExecuteAsync() //执行具体操作
{
// 这里可以添加你需要执行的具体操作
// 例如:导出统计数据到文件等
//await Task.Delay(1000); //模拟异步操作
var items = ctx.Users.GroupBy(u => u.UserName).Select(g => new
{
UserName = g.Key, //分组的键
Count = g.Count() //统计每个用户的数量
}); // 分组统计
StringBuilder sb = new StringBuilder(); //创建StringBuilder对象
sb.AppendLine($"Date:{DateTime.Now}"); //添加当前日期
foreach (var item in items) // 遍历统计结果
{
sb.Append(item.UserName).AppendLine($":{item.Count}"); //添加用户名和数量
sb.AppendLine(); //添加空行
}
await File.WriteAllTextAsync("D:/SqlServer.txt", sb.ToString()); //写入文件
_logger.LogInformation("导出统计数据成功"); //输出日志
}
public override void Dispose()
{
base.Dispose(); //释放资源
serviceScope.Dispose(); //释放服务范围
}
}

请求数据的校验

.NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations命名空间下定义了非常多的校验规则Attribute,比如[Required]用来设置值必须是非空的、[EmailAddress]用来设置值必须是Email格式的、[RegularExpression]用来根据给定的正则表达式对数据进行校验。我们也可以使用CustomValidationAttribute或者模型类实现IValidatableObject接口来编写自定义的校验规则。

.NET Core内置的校验机制有以下几个问题

  1. 无论是通过在属性上标注校验规则Attribute的方法,还是实现IValidatableObject接口的方式,我们的校验规则都是和模型类耦合在一起的,这违反了面向对象的“单一职责原则”。
  2. .NET Core中内置的校验规则不够多,很多常用的校验需求都需要我们编写自定义校验规则。

FluentValidation的基本使用

  1. FluentValidation:用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中。
  2. 在项目中安装NuGet包:
    1
    FluentValidation.AspNetCore

第一步,在Program.cs中添加注册相关服务的代码。

1
2
3
builder.Services.AddFluentValidationAutoValidation(); //注册FluentValidation自动验证
builder.Services.AddFluentValidationClientsideAdapters(); //注册FluentValidation客户端验证
builder.Services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); //注册所有程序集中的验证器

AddValidatorsFromAssemblies方法用于把指定程序集中所有实现了IValidator接口的数据校验类注册到依赖注入容器中。

第二步,编写一个模型类Login2Request。

1
public record Login2Request(string Email,string Password,string Password2); //定义登录请求记录

第三步,编写一个继承自AbstractValidator的数据校验类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Login2RequestValidator : AbstractValidator<Login2Request>
{
public Login2RequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("邮箱不能为空")
.EmailAddress().WithMessage("邮箱格式不正确");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空")
.MinimumLength(6).WithMessage("密码长度不能小于6位");
RuleFor(x => x.Password2)
.NotEmpty().WithMessage("确认密码不能为空")
.Equal(x => x.Password).WithMessage("两次输入的密码不一致");
}
}
1
2
3
4
5
6
7
8
public class ValuesController : ControllerBase
{
[HttpPost]
public ActionResult Login(Login2Request request)
{
return Ok(request); //返回请求对象
}
}

FluentValidation中的注入服务

在编写数据校验代码的时候,有时候我们需要调用依赖注入容器中的服务,FluentValidation中的数据校验类是通过依赖注入容器实例化的,因此我们同样可以通过构造方法来数据校验中注入服务。

1
2
3
4
5
6
7
8
9
public class Login3RequestValidator : AbstractValidator<Login3Request>
{
public Login3RequestValidator(MyDbContext dbCtx)
{
RuleFor(x => x.UserName).NotNull()
.MustAsync((name,_) =>dbCtx.Users.AnyAsync(u => u.UserName == name))
.WithMessage(c => $"用户名{c.UserName}不存在");
}
}