托管(轮询)服务
- 场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据在缓存,每天凌晨3点把数据导出到备份数据库,每隔5秒钟在两张表之间同步一次数据。
- 托管服务实现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); string s = await File.ReadAllTextAsync("D:/SqlServer.txt"); await Task.Delay(3000); _logger.LogInformation(s); } }
|
调用AddHostedService到依赖注入容器中
1
| builder.Services.AddHostedService<DemoBgService>();
|
托管服务的异常问题
- 从.NET 6开始,当托管服务发生未处理异常的时候,程序就会自动停止并退出。可以把HostOptions.BackgroundServiceExceptionBehavior设置为lgnore,程序会忽略异常,而不是停止程序。不过推荐采用默认的设置,因为“异常应该被妥善的处理,而不是被忽略”。
- 要在ExecuteAsync方法中把代码用try…catch包裹起来,当发生异常的时候,记录日志中或发生报警等。
1 2 3 4 5 6 7 8 9 10 11
| try { await Task.Delay(3000); string s = await File.ReadAllTextAsync("D:/SqlServer.txt"); await Task.Delay(3000); _logger.LogInformation(s); } catch (Exception ex) { Console.WriteLine("程序中出现异常"+ex); }
|
托管服务中使用依赖注入的陷阱
托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入生命周期为范围或者瞬太的服务。比如注入EF Core的上下文的话,程序就会抛出异常。
可以通过构造方法注入一个IServiceScopeFactory服务,它可以用来创建一个IServiceScope对象,这样我们就可以通过IServiceScope来创建短生命周全的服务了。记得再Dispose中释放IServiceScope。
下面实现一个常驻后台的托管服务,它实现的功能是每隔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); } catch (Exception ex) { _logger.LogError(ex, "程序中出现异常"); await Task.Delay(3000); } } } private async Task DoExecuteAsync() { var items = ctx.Users.GroupBy(u => u.UserName).Select(g => new { UserName = g.Key, Count = g.Count() }); StringBuilder sb = new 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内置的校验机制有以下几个问题
- 无论是通过在属性上标注校验规则Attribute的方法,还是实现IValidatableObject接口的方式,我们的校验规则都是和模型类耦合在一起的,这违反了面向对象的“单一职责原则”。
- .NET Core中内置的校验规则不够多,很多常用的校验需求都需要我们编写自定义校验规则。
FluentValidation的基本使用
- FluentValidation:用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中。
- 在项目中安装NuGet包:
1
| FluentValidation.AspNetCore
|
第一步,在Program.cs中添加注册相关服务的代码。
1 2 3
| builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddFluentValidationClientsideAdapters(); 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}不存在"); } }
|