第四十六节:后台托管服务(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

    }

 

 

 

二. 内置数据校验

 

 

 

 

 

 

三. FluentValidition

 

 

 

 

 

 

四. 程序发布部署

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章