• 第四十六节:后台托管服务(DI的两种写法)、数据校验规则(内置、 FluentValidation)、程序发布部署


    一. 托管服务

    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
    
        }

    二. 内置数据校验

    1. 说明

        NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations这个命名空间下,比如

        [Required] 必填

        [EmailAddress] 邮箱地址,默认的验证规则很简单,只要符合xxx@xxx即可,通常配合[RegularExpression] 正则验证

        [RegularExpression] 正则验证

        [StringLength(10, MinimumLength = 3)] 长度验证(最大长度最小长度)

        [Compare(nameof(Password2), ErrorMessage = "两次密码必须一致")] 用于比较两个值是否相同

        此外还有: CustomValidationAttribute 、 IValidatableObject

    2. 存在的问题

       A. 很多常用的校验都需要编写自定义校验规则,而且写起来麻烦

       B. 校验规则都是和模型类耦合在一起,违反“单一职责原则”

     PS:这种内置的校验规则,由于和模型类耦合在一起,通常用在action的接收参数上,并不用在EFCore实体模型上

    3. 实操

      A. 编写实体模型UserInfo, 配置校验规则

      public class UserInfo
        {
            [Required]
            public string userName { get; set; }
    
            [Required]
            [StringLength(10,MinimumLength =4,ErrorMessage ="密码长度应该为4-10位")]
            public string pwd1 { get; set; }
    
            [Compare(nameof(pwd1),ErrorMessage ="两次密码必须相同")]
            public string pwd2 { get; set; }
    
            [Required]
            [EmailAddress]
            [RegularExpression("^.*@(qq|163)\\.com$", ErrorMessage = "只支持QQ邮箱和163邮箱")]
            public string email { get; set; }
    
        }

      B. 编写注册方法Register,用UserInfo接收

            [HttpPost]
            public string Register(UserInfo user)
            {
    
                return $"注册成功:userName:{user.userName},pwd:{user.pwd1},email:{user.email}";
            }

      C. 测试

       ①. 不填写userName,结果如图所示,校验不通过

       ②. 两次pwd不一致、邮箱格式不正确,结果如图所示,校验不通过

     

    4. 配合axios测试

        400 Bad Request 是由于明显的客户端错误(例如,格式错误的请求语法,太大的大小,无效的请求消息或欺骗性路由请求),服务器不能或不会处理该请求。

        校验不通过,报400错误,进入的axios的catch中哦

    代码如下:

    详见 00-VueTest,注意:这里并没有改变request.js中的封装,只是在调用代码中通过catch获取打印了一下,届时根据实际情况考虑如何封装即可

    <script setup name="xxxx">
    import { myAxios1, myAxios2 } from "@/utils/request";
    const myBaseUrl = "http://localhost:5244";
    
    const checkData = async () => {
    	const result1 = await myAxios2({
    		baseURL: myBaseUrl,
    		url: "api/Test/Register",
    		method: "post",
    		data: {
    			userName: "ypf",
    			pwd1: "123456",
    			pwd2: "123456",
    			email: "ypf@qq.com",
    		},
    	}).catch(error => {
    		console.log(error.data.status);
    		console.log(error.data.errors);
    	});
    
    	console.log(" 结果为:" + result1);
    };
    </script>

    运行结果:

    三. FluentValidation

    1.说明

       用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中

      【官网:https://docs.fluentvalidation.net/en/latest/】

    2.常用的校验方法

        RuleFor:表示规则作用于哪个字段

        NotEmpty和NotNull:表示非空验证,其中NotEmpty更加严格,比如:null、空字符串、空格、空集合、类型的默认值 都认为格式错误

        WithMessage:用于提示客户端错误原因, 注意可以出现多次,加载哪个规则的后面则为谁提示

        Length:长度验证

        Equal和NotEqual:相等 或 不相等

        Must:自定义验证规则,可以直接写,也可以传入一个验证函数

        when:条件验证

        ....等等, 更多验证规则详见官网

    3.基本使用

      A. 安装程序集【FluentValidation.AspNetCore 11.0.2】

      B. 编写 UserInfoValidate校验类, 必须继承泛型类 AbstractValidator<T>, 其中T代表需要被校验的类

    校验类代码

     public class UserInfoValidate : AbstractValidator<UserInfo>
        {
            public UserInfoValidate()
            {
                //注意WithMessage跟在谁后面则为谁提示
                RuleFor(x => x.userName).NotEmpty().WithMessage("userName不能为空")
                                        .Length(4, 10).WithMessage("userName长度必须为4-10位");
    
                RuleFor(x => x.pwd1).NotEmpty().WithMessage("pwd1不能为空")
                                     .Length(3, 10).WithMessage("pwd1长度必须为3-10位");
    
                RuleFor(x => x.pwd2).Equal(u => u.pwd1).WithMessage("pwd1和pwd2的值必须相同");
    
                //RuleFor(x => x.email).Must(u => u.EndsWith("@163.com") || u.EndsWith("@qq.com")).WithMessage("仅支持qq邮箱和163邮箱");
    
                RuleFor(x => x.email).Must(checkEmail).WithMessage("邮箱格式不正确");
    
            }
    
            /// <summary>
            /// 自定义方法校验
            /// </summary>
            /// <param name="value"></param>
            /// <returns></returns>
            private bool checkEmail(string value)
            {
                if (value.Length > 4 && value.Contains("@"))
                {
                    return true;
                }
                return false;
            }
        }

    实体类代码

     public class UserInfo
        {
            public string userName { get; set; }
            public string pwd1 { get; set; }
            public string pwd2 { get; set; }
            public string email { get; set; }
        }

      C. 在Program通过反射的模式注册所有实现了AbstractValidator的校验规则类 

    //注册所有实现了AbstractValidator的校验规则类
    builder.Services.AddFluentValidation(fv => {
        Assembly assembly = Assembly.GetExecutingAssembly();
        fv.RegisterValidatorsFromAssembly(assembly);
    });

      D.编写Register方法进行测试

        ① 漏掉userName,且两次密码不一致

        ② 测试通过

    PS:这里返回值和内置验证相同,所以客户端如果用axios处理的模式也相同

            [HttpPost]
            public string Register(UserInfo user)
            {
    
                return $"注册成功:userName:{user.userName},pwd:{user.pwd1},email:{user.email}";
            }

    4. 注入服务

      比如将MyDbContext先在Program中注册一下,然后在UserInfoValidate的构造函数中注入使用即可,详见代码

    MyDbContext代码

      public class MyDbContext
        {
            public List<string> name { get; set; }
            public MyDbContext()
            {
                name = new List<string>()
                {
                    "ypf1",
                    "ypf2",
                    "admin"
                };
            }
            public bool isContains(string userName)
            {
                return this.name.Contains(userName);    
            }
        }

    注册代码

    builder.Services.AddScoped<MyDbContext>();

    构造函数注入代码

            public UserInfoValidate(MyDbContext db)
            {
                //测试注入
                RuleFor(x => x.userName).Must(db.isContains).WithMessage("该userName数据库中不存在");
    
                RuleFor(x => x.pwd1).NotEmpty().WithMessage("pwd1不能为空")
                                     .Length(3, 10).WithMessage("pwd1长度必须为3-10位");
    
                RuleFor(x => x.pwd2).Equal(u => u.pwd1).WithMessage("pwd1和pwd2的值必须相同");
    
                //RuleFor(x => x.email).Must(u => u.EndsWith("@163.com") || u.EndsWith("@qq.com")).WithMessage("仅支持qq邮箱和163邮箱");
    
                RuleFor(x => x.email).Must(checkEmail).WithMessage("邮箱格式不正确");
    
    
            }

    四. 程序发布部署

    1. 两种发布模式

     (1). 独立部署:将所需要的依赖环境和发布包一起打包起来

     (2). 框架依赖:需要服务器上安装.Net环境

    2. 部署环境

    (1). windows+IIS:

    (2). linux+nginx

    (3). k8s

    (4). Kestrel直接使用:做好的发布包在window环境下, 点击exe程序直接就能运行,这就是因为内置了Kestrel服务器的缘故

    PS:尽管Kestrel已经强大到足以作为一个独立的Web服务器被使用了,但是一般仍然不会让Kestrel直接面对终端用户的请求

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    适配器模式(2)
    设计模式之6大设计原则(1)
    Mybatis框架基础支持层——反射工具箱之MetaClass(7)
    Mybatis框架基础支持层——反射工具箱之实体属性Property工具集(6)
    Mybatis框架基础支持层——反射工具箱之对象工厂ObjectFactory&DefaultObjectFactory(5)
    Mybatis框架基础支持层——反射工具箱之泛型解析工具TypeParameterResolver(4)
    Guava动态调用方法
    数据库的数据同步
    springboot(二十二)-sharding-jdbc-读写分离
    springboot(二十一)-集成memcached
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/16328198.html
Copyright © 2020-2023  润新知