当前位置 博文首页 > 启天:C#编写一个在asp.net core 3.1下的简单的corn模式的计划任

    启天:C#编写一个在asp.net core 3.1下的简单的corn模式的计划任

    作者:启天 时间:2021-01-31 16:24

    asp.net core 下,新增了一个BackgroundService用来实现能在后台跑一个长久运行的任务,因此,也可以用来替换掉原来使用的static的Timer组件,

    Timer组件主要有以下几个麻烦的地方

    1.如果是需要长时间跑的定时任务,需要定义为static,,在asp.net core下,无法利用到DI,无法从DI中获取DbContext之类的

    2.启动定时器的时候,需要在start.cs自己手动启动

    3.Timer是传入处理函数的方式,如果有好几个定时器,拼在一起,代码会看起来比较乱

    4.使用Timer也无法实现corn表达式

    5.Timer中也无法使用async异步处理

     

    首先,我们来实现一个简单的定时器功能,用来替换掉Timer类:

    1.TimerHostedService 定时器的基类

     1 /// <summary>
     2     /// 用于在后台执行一个定时任务,用于取代TimeEx,在asp.net core的环境下使用,继承该类后,使用 services.AddHostedService<当前类类型>();后,自动在后台启动当前定时任务
     3     /// </summary>
     4     public abstract class TimerHostedService : BackgroundService
     5     {
     6         private IServiceProvider _provider = null;
     7 
     8         protected TimerHostedService(IServiceProvider provider)
     9         {
    10             _provider = provider;
    11         }
    12 
    13         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    14         {
    15             if (Enabled && Internal>0)  //如果启动服务的时候,Enabled为false或者不设置间隔时间,则该定时器永久不启动
    16             {
    17                 while (!stoppingToken.IsCancellationRequested)  //如果站点触发停止,则会使用stoppingToken的IsCancellactionRequest判断是否由IIS之类的停止应用
    18                 {
    19                     await Task.Delay(Internal, stoppingToken);
    20 
    21                     if (!stoppingToken.IsCancellationRequested)
    22                     {
    23                         using (var scope = _provider.CreateScope())
    24                         {
    25                             try
    26                             {
    27                                 await Run(scope.ServiceProvider, stoppingToken);
    28                             }
    29                             catch (Exception e)
    30                             {
    31 
    32                             }
    33                         }
    34                     }
    35 
    36                     
    37                 }
    38             }
    39 
    40             
    41 
    42             return;
    43         }
    44 
    45         /// <summary>
    46         /// 实际执行的定时器处理函数
    47         /// </summary>
    48         /// <param name="serviceScope">当次的Ioc容器,可获取当前程序中用于注入的容器内的类</param>
    49         /// <param name="stoppingToken">是否暂停</param>
    50         /// <returns></returns>
    51         protected abstract Task Run(IServiceProvider serviceProvider, CancellationToken stoppingToken);
    52 
    53         /// <summary>
    54         /// 定时器间隔触发时间,单位是ms
    55         /// </summary>
    56         protected abstract int Internal {  get; }
    57 
    58         /// <summary>
    59         /// 当前定时器是否启用,true为定时器有效,false为停用
    60         /// </summary>
    61         public virtual bool Enabled { set; get; } = true;
    62     }

    2.实现一个定时器:

     1 /// <summary>
     2     /// 检查订单是否完成的后台任务
     3     /// </summary>
     4     public class CheckOrderCompleteTask:TimerHostedService
     5     {
     6         public CheckOrderCompleteTask(IServiceProvider provider) : base(provider)
     7         {
     8             this.Enabled = CustomConfigManager.Default["Timer:CheckBlessCompleted"].ToBool();
     9         }
    10 
    11         protected override async Task Run(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    12         {
    13             var s = (XXXService) serviceProvider.GetService(typeof(XXXService)); //可以从DI容器中获取service或者dbcontext
    17             await s.CheckXXX(stoppingToken);  //此处为实际执行的定时任务处理函数
    18         }
    19 
    20         protected override int Internal { get; } = 1000 * 60 * 5;
    21     }

        在Start.cs中,注册该任务:

    1 public void ConfigureServices(IServiceCollection services)
    2 {
    3     //.....其他代码
    4     services.AddHostedService<CheckOrderCompleteTask>(); //此处将服务注册后,即由asp.net core在启动后,自动启动该服务
    5 }

         这样,就会有asp.net core自动启动该任务

     

    由于上面定义的是一个定时器,有时候需要比如半夜12点,或者中午12点运行,这种场景下,就需要使用到计划任务的组件了,,.net下,常用的,一般有hangfire跟Quartz.Net,,这两个组件功能比较完善,而且也带有管理功能,but,..就是有时候复杂了点,通常有些不复杂的计划任务,比如又不想直接引入那么复杂的组件,那么可以根据上面的定时组件,变化出一个简单的计划任务组件:

        /// <summary>
        /// 一个简单的corn模式的计划任务<br/>用于在一些已知的计划时间执行某些任务的情况下使用,Cron属性在服务启动后,变无法修改,如需配置运行时可修改,请使用Hangfire之类的其他第三方框架
        /// </summary>
        public abstract class SimpleScheduledTaskService : BackgroundService
        {
            private IServiceProvider _provider = null;
            private CrontabSchedule _crontab = null;
            private string _cron;
            private bool _enabled=true;
            private bool _isInited = false;
    
            protected SimpleScheduledTaskService(IServiceProvider provider)
            {
                _provider = provider;
            }
    
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                try
                {
                    _crontab = CrontabSchedule.Parse(_cron);  //解析cron字符串
                }
                catch (Exception e)
                {
                    throw;
                }
    
                while (Enabled && _crontab != null && !stoppingToken.IsCancellationRequested)
                {
    
                    var nextDt = _crontab.GetNextOccurrence(DateTime.Now.AddSeconds(2));
    
                    var interval = (nextDt - DateTime.Now);
    
                    await Task.Delay(interval, stoppingToken);
    
                    var logger = (ILogger)_provider.GetService(typeof(ILogger));
    
                    try
                    {
                        logger?.Log(LogLevel.Trace, $"启动计划任务:{this.GetType().Name}");
    
                        await Run(_provider, stoppingToken);
    
                        logger?.Log(LogLevel.Trace, $"完成计划任务:{this.GetType().Name}");
                    }
                    catch (Exception e)
                    {
                        logger?.Log(LogLevel.Error, e, $"计划任务执行异常:{e.Message}");
                    }
                }
            }
    
            protected abstract Task Run(IServiceProvider provider, CancellationToken stoppingToken);
    
            /// <summary>
            /// 计划任务的Cron配置字符串,可使用在线生成器生成后,填入
            /// </summary>
            public virtual string Cron
            {
                get => _cron;
            }
    
            /// <summary>
            /// 计划任务是否启动
            /// </summary>
            public virtual bool Enabled
            {
                set => _enabled = value;
                get => _enabled;
            }
        }

    上述使用类似Timer的方式,,通过计算cron表达式计算后的结果与当前时间差,delay指定时间后触发,这个功能,一般只能用在一些不是特别重要的定时任务,并且不需要补偿的环境下

    通常我都是用比如:https://cron.qqe2.com/之类的在线生成cron表达式的网站生成

     

    计划任务使用的第三方组件为:

    NCrontab : https://github.com/atifaziz/NCrontab

     

    上述源码地址:

    TimerHostedService: https://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Services/TimerHostedService.cs

    SimpleScheduledTaskService : https://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Services/ScheduledTaskService.cs

    bk