.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件
阅读原文时间:2023年07月10日阅读:1

常用的定时任务组件有 Quartz.Net 和 Hangfire 两种,这两种是使用人数比较多的定时任务组件,个人以前也是使用的 Hangfire ,慢慢的发现自己想要的其实只是一个能够根据 Cron 表达式来定时执行函数的功能,Quartz.Net 和 Hangfire 虽然都能实现这个目的,但是他们都只用来实现 Cron表达式解析定时执行函数就显得太笨重了,所以想着以 解析 Cron表达式定期执行函数为目的,编写了下面的一套逻辑。

首先为了解析 Cron表达式,我们需要一个CronHelper ,代码如下

using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace Common
{

public class CronHelper  
{

    /// <summary>  
    /// 获取当前时间之后下一次触发时间  
    /// </summary>  
    /// <param name="cronExpression"></param>  
    /// <returns></returns>  
    public static DateTimeOffset GetNextOccurrence(string cronExpression)  
    {  
        return GetNextOccurrence(cronExpression, DateTimeOffset.UtcNow);  
    }

    /// <summary>  
    /// 获取给定时间之后下一次触发时间  
    /// </summary>  
    /// <param name="cronExpression"></param>  
    /// <param name="afterTimeUtc"></param>  
    /// <returns></returns>  
    public static DateTimeOffset GetNextOccurrence(string cronExpression, DateTimeOffset afterTimeUtc)  
    {  
        return new CronExpression(cronExpression).GetTimeAfter(afterTimeUtc)!.Value;  
    }

    /// <summary>  
    /// 获取当前时间之后N次触发时间  
    /// </summary>  
    /// <param name="cronExpression"></param>  
    /// <param name="count"></param>  
    /// <returns></returns>  
    public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, int count)  
    {  
        return GetNextOccurrences(cronExpression, DateTimeOffset.UtcNow, count);  
    }

    /// <summary>  
    /// 获取给定时间之后N次触发时间  
    /// </summary>  
    /// <param name="cronExpression"></param>  
    /// <param name="afterTimeUtc"></param>  
    /// <returns></returns>  
    public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, DateTimeOffset afterTimeUtc, int count)  
    {  
        CronExpression cron = new(cronExpression);

        List<DateTimeOffset> dateTimeOffsets = new();

        for (int i = 0; i < count; i++)  
        {  
            afterTimeUtc = cron.GetTimeAfter(afterTimeUtc)!.Value;

            dateTimeOffsets.Add(afterTimeUtc);  
        }

        return dateTimeOffsets;  
    }

    private class CronExpression  
    {

        private const int Second = 0;

        private const int Minute = 1;

        private const int Hour = 2;

        private const int DayOfMonth = 3;

        private const int Month = 4;

        private const int DayOfWeek = 5;

        private const int Year = 6;

        private const int AllSpecInt = 99;

        private const int NoSpecInt = 98;

        private const int AllSpec = AllSpecInt;

        private const int NoSpec = NoSpecInt;

        private SortedSet<int> seconds = null!;

        private SortedSet<int> minutes = null!;

        private SortedSet<int> hours = null!;

        private SortedSet<int> daysOfMonth = null!;

        private SortedSet<int> months = null!;

        private SortedSet<int> daysOfWeek = null!;

        private SortedSet<int> years = null!;

        private bool lastdayOfWeek;

        private int everyNthWeek;

        private int nthdayOfWeek;

        private bool lastdayOfMonth;

        private bool nearestWeekday;

        private int lastdayOffset;

        private static readonly Dictionary<string, int> monthMap = new Dictionary<string, int>(20);

        private static readonly Dictionary<string, int> dayMap = new Dictionary<string, int>(60);

        private static readonly int MaxYear = DateTime.Now.Year + 100;

        private static readonly char\[\] splitSeparators = { ' ', '\\t', '\\r', '\\n' };

        private static readonly char\[\] commaSeparator = { ',' };

        private static readonly Regex regex = new Regex("^L-\[0-9\]\*\[W\]?", RegexOptions.Compiled);

        private static readonly TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local;

        public CronExpression(string cronExpression)  
        {  
            if (monthMap.Count == 0)  
            {  
                monthMap.Add("JAN", 0);  
                monthMap.Add("FEB", 1);  
                monthMap.Add("MAR", 2);  
                monthMap.Add("APR", 3);  
                monthMap.Add("MAY", 4);  
                monthMap.Add("JUN", 5);  
                monthMap.Add("JUL", 6);  
                monthMap.Add("AUG", 7);  
                monthMap.Add("SEP", 8);  
                monthMap.Add("OCT", 9);  
                monthMap.Add("NOV", 10);  
                monthMap.Add("DEC", 11);

                dayMap.Add("SUN", 1);  
                dayMap.Add("MON", 2);  
                dayMap.Add("TUE", 3);  
                dayMap.Add("WED", 4);  
                dayMap.Add("THU", 5);  
                dayMap.Add("FRI", 6);  
                dayMap.Add("SAT", 7);  
            }

            if (cronExpression == null)  
            {  
                throw new ArgumentException("cronExpression 不能为空");  
            }

            CronExpressionString = CultureInfo.InvariantCulture.TextInfo.ToUpper(cronExpression);  
            BuildExpression(CronExpressionString);  
        }

        /// <summary>  
        /// 构建表达式  
        /// </summary>  
        /// <param name="expression"></param>  
        /// <exception cref="FormatException"></exception>  
        private void BuildExpression(string expression)  
        {  
            try  
            {  
                seconds ??= new SortedSet<int>();  
                minutes ??= new SortedSet<int>();  
                hours ??= new SortedSet<int>();  
                daysOfMonth ??= new SortedSet<int>();  
                months ??= new SortedSet<int>();  
                daysOfWeek ??= new SortedSet<int>();  
                years ??= new SortedSet<int>();

                int exprOn = Second;

                string\[\] exprsTok = expression.Split(splitSeparators, StringSplitOptions.RemoveEmptyEntries);  
                foreach (string exprTok in exprsTok)  
                {  
                    string expr = exprTok.Trim();

                    if (expr.Length == 0)  
                    {  
                        continue;  
                    }  
                    if (exprOn > Year)  
                    {  
                        break;  
                    }

                    if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)  
                    {  
                        throw new FormatException("不支持在月份的其他日期指定“L”和“LW”");  
                    }  
                    if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)  
                    {  
                        throw new FormatException("不支持在一周的其他日期指定“L”");  
                    }  
                    if (exprOn == DayOfWeek && expr.IndexOf('#') != -1 && expr.IndexOf('#', expr.IndexOf('#') + 1) != -1)  
                    {  
                        throw new FormatException("不支持指定多个“第N”天。");  
                    }

                    string\[\] vTok = expr.Split(commaSeparator);  
                    foreach (string v in vTok)  
                    {  
                        StoreExpressionVals(0, v, exprOn);  
                    }

                    exprOn++;  
                }

                if (exprOn <= DayOfWeek)  
                {  
                    throw new FormatException("表达式意料之外的结束。");  
                }

                if (exprOn <= Year)  
                {  
                    StoreExpressionVals(0, "\*", Year);  
                }

                var dow = GetSet(DayOfWeek);  
                var dom = GetSet(DayOfMonth);

                bool dayOfMSpec = !dom.Contains(NoSpec);  
                bool dayOfWSpec = !dow.Contains(NoSpec);

                if (dayOfMSpec && !dayOfWSpec)  
                {  
                    // skip  
                }  
                else if (dayOfWSpec && !dayOfMSpec)  
                {  
                    // skip  
                }  
                else  
                {  
                    throw new FormatException("不支持同时指定星期和日参数。");  
                }  
            }  
            catch (FormatException)  
            {  
                throw;  
            }  
            catch (Exception e)  
            {  
                throw new FormatException($"非法的 cron 表达式格式 ({e.Message})", e);  
            }  
        }

        /// <summary>  
        /// Stores the expression values.  
        /// </summary>  
        /// <param name="pos">The position.</param>  
        /// <param name="s">The string to traverse.</param>  
        /// <param name="type">The type of value.</param>  
        /// <returns></returns>  
        private int StoreExpressionVals(int pos, string s, int type)  
        {  
            int incr = 0;  
            int i = SkipWhiteSpace(pos, s);  
            if (i >= s.Length)  
            {  
                return i;  
            }  
            char c = s\[i\];  
            if (c >= 'A' && c <= 'Z' && !s.Equals("L") && !s.Equals("LW") && !regex.IsMatch(s))  
            {  
                string sub = s.Substring(i, 3);  
                int sval;  
                int eval = -1;  
                if (type == Month)  
                {  
                    sval = GetMonthNumber(sub) + 1;  
                    if (sval <= 0)  
                    {  
                        throw new FormatException($"无效的月份值:'{sub}'");  
                    }  
                    if (s.Length > i + 3)  
                    {  
                        c = s\[i + 3\];  
                        if (c == '-')  
                        {  
                            i += 4;  
                            sub = s.Substring(i, 3);  
                            eval = GetMonthNumber(sub) + 1;  
                            if (eval <= 0)  
                            {  
                                throw new FormatException(  
                                    $"无效的月份值: '{sub}'");  
                            }  
                        }  
                    }  
                }  
                else if (type == DayOfWeek)  
                {  
                    sval = GetDayOfWeekNumber(sub);  
                    if (sval < 0)  
                    {  
                        throw new FormatException($"无效的星期几值: '{sub}'");  
                    }  
                    if (s.Length > i + 3)  
                    {  
                        c = s\[i + 3\];  
                        if (c == '-')  
                        {  
                            i += 4;  
                            sub = s.Substring(i, 3);  
                            eval = GetDayOfWeekNumber(sub);  
                            if (eval < 0)  
                            {  
                                throw new FormatException(  
                                    $"无效的星期几值: '{sub}'");  
                            }  
                        }  
                        else if (c == '#')  
                        {  
                            try  
                            {  
                                i += 4;  
                                nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);  
                                if (nthdayOfWeek is < 1 or > 5)  
                                {  
                                    throw new FormatException("周的第n天小于1或大于5");  
                                }  
                            }  
                            catch (Exception)  
                            {  
                                throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");  
                            }  
                        }  
                        else if (c == '/')  
                        {  
                            try  
                            {  
                                i += 4;  
                                everyNthWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);  
                                if (everyNthWeek is < 1 or > 5)  
                                {  
                                    throw new FormatException("每个星期<1或>5");  
                                }  
                            }  
                            catch (Exception)  
                            {  
                                throw new FormatException("1 到 5 之间的数值必须跟在 '/' 选项后面");  
                            }  
                        }  
                        else if (c == 'L')  
                        {  
                            lastdayOfWeek = true;  
                            i++;  
                        }  
                        else  
                        {  
                            throw new FormatException($"此位置的非法字符:'{sub}'");  
                        }  
                    }  
                }  
                else  
                {  
                    throw new FormatException($"此位置的非法字符:'{sub}'");  
                }  
                if (eval != -1)  
                {  
                    incr = 1;  
                }  
                AddToSet(sval, eval, incr, type);  
                return i + 3;  
            }

            if (c == '?')  
            {  
                i++;  
                if (i + 1 < s.Length && s\[i\] != ' ' && s\[i + 1\] != '\\t')  
                {  
                    throw new FormatException("'?' 后的非法字符: " + s\[i\]);  
                }  
                if (type != DayOfWeek && type != DayOfMonth)  
                {  
                    throw new FormatException(  
                        "'?' 只能为月日或周日指定。");  
                }  
                if (type == DayOfWeek && !lastdayOfMonth)  
                {  
                    int val = daysOfMonth.LastOrDefault();  
                    if (val == NoSpecInt)  
                    {  
                        throw new FormatException(  
                            "'?' 只能为月日或周日指定。");  
                    }  
                }

                AddToSet(NoSpecInt, -1, 0, type);  
                return i;  
            }

            var startsWithAsterisk = c == '\*';  
            if (startsWithAsterisk || c == '/')  
            {  
                if (startsWithAsterisk && i + 1 >= s.Length)  
                {  
                    AddToSet(AllSpecInt, -1, incr, type);  
                    return i + 1;  
                }  
                if (c == '/' && (i + 1 >= s.Length || s\[i + 1\] == ' ' || s\[i + 1\] == '\\t'))  
                {  
                    throw new FormatException("'/' 后面必须跟一个整数。");  
                }  
                if (startsWithAsterisk)  
                {  
                    i++;  
                }  
                c = s\[i\];  
                if (c == '/')  
                {  
                    // is an increment specified?  
                    i++;  
                    if (i >= s.Length)  
                    {  
                        throw new FormatException("字符串意外结束。");  
                    }

                    incr = GetNumericValue(s, i);

                    i++;  
                    if (incr > 10)  
                    {  
                        i++;  
                    }  
                    CheckIncrementRange(incr, type);  
                }  
                else  
                {  
                    if (startsWithAsterisk)  
                    {  
                        throw new FormatException("星号后的非法字符:" + s);  
                    }  
                    incr = 1;  
                }

                AddToSet(AllSpecInt, -1, incr, type);  
                return i;  
            }  
            if (c == 'L')  
            {  
                i++;  
                if (type == DayOfMonth)  
                {  
                    lastdayOfMonth = true;  
                }  
                if (type == DayOfWeek)  
                {  
                    AddToSet(7, 7, 0, type);  
                }  
                if (type == DayOfMonth && s.Length > i)  
                {  
                    c = s\[i\];  
                    if (c == '-')  
                    {  
                        ValueSet vs = GetValue(0, s, i + 1);  
                        lastdayOffset = vs.theValue;  
                        if (lastdayOffset > 30)  
                        {  
                            throw new FormatException("与最后一天的偏移量必须 <= 30");  
                        }  
                        i = vs.pos;  
                    }  
                    if (s.Length > i)  
                    {  
                        c = s\[i\];  
                        if (c == 'W')  
                        {  
                            nearestWeekday = true;  
                            i++;  
                        }  
                    }  
                }  
                return i;  
            }  
            if (c >= '0' && c <= '9')  
            {  
                int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);  
                i++;  
                if (i >= s.Length)  
                {  
                    AddToSet(val, -1, -1, type);  
                }  
                else  
                {  
                    c = s\[i\];  
                    if (c >= '0' && c <= '9')  
                    {  
                        ValueSet vs = GetValue(val, s, i);  
                        val = vs.theValue;  
                        i = vs.pos;  
                    }  
                    i = CheckNext(i, s, val, type);  
                    return i;  
                }  
            }  
            else  
            {  
                throw new FormatException($"意外字符:{c}");  
            }

            return i;  
        }

        // ReSharper disable once UnusedParameter.Local  
        private static void CheckIncrementRange(int incr, int type)  
        {  
            if (incr > 59 && (type == Second || type == Minute))  
            {  
                throw new FormatException($"增量 > 60 : {incr}");  
            }  
            if (incr > 23 && type == Hour)  
            {  
                throw new FormatException($"增量 > 24 : {incr}");  
            }  
            if (incr > 31 && type == DayOfMonth)  
            {  
                throw new FormatException($"增量 > 31 : {incr}");  
            }  
            if (incr > 7 && type == DayOfWeek)  
            {  
                throw new FormatException($"增量 > 7 : {incr}");  
            }  
            if (incr > 12 && type == Month)  
            {  
                throw new FormatException($"增量 > 12 : {incr}");  
            }  
        }

        /// <summary>  
        /// Checks the next value.  
        /// </summary>  
        /// <param name="pos">The position.</param>  
        /// <param name="s">The string to check.</param>  
        /// <param name="val">The value.</param>  
        /// <param name="type">The type to search.</param>  
        /// <returns></returns>  
        private int CheckNext(int pos, string s, int val, int type)  
        {  
            int end = -1;  
            int i = pos;

            if (i >= s.Length)  
            {  
                AddToSet(val, end, -1, type);  
                return i;  
            }

            char c = s\[pos\];

            if (c == 'L')  
            {  
                if (type == DayOfWeek)  
                {  
                    if (val < 1 || val > 7)  
                    {  
                        throw new FormatException("星期日值必须介于1和7之间");  
                    }  
                    lastdayOfWeek = true;  
                }  
                else  
                {  
                    throw new FormatException($"'L' 选项在这里无效。(位置={i})");  
                }  
                var data = GetSet(type);  
                data.Add(val);  
                i++;  
                return i;  
            }

            if (c == 'W')  
            {  
                if (type == DayOfMonth)  
                {  
                    nearestWeekday = true;  
                }  
                else  
                {  
                    throw new FormatException($"'W' 选项在这里无效。 (位置={i})");  
                }  
                if (val > 31)  
                {  
                    throw new FormatException("'W' 选项对于大于 31 的值(一个月中的最大天数)没有意义");  
                }

                var data = GetSet(type);  
                data.Add(val);  
                i++;  
                return i;  
            }

            if (c == '#')  
            {  
                if (type != DayOfWeek)  
                {  
                    throw new FormatException($"'#' 选项在这里无效。 (位置={i})");  
                }  
                i++;  
                try  
                {  
                    nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);  
                    if (nthdayOfWeek is < 1 or > 5)  
                    {  
                        throw new FormatException("周的第n天小于1或大于5");  
                    }  
                }  
                catch (Exception)  
                {  
                    throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");  
                }

                var data = GetSet(type);  
                data.Add(val);  
                i++;  
                return i;  
            }

            if (c == 'C')  
            {  
                if (type == DayOfWeek)  
                {

                }  
                else if (type == DayOfMonth)  
                {

                }  
                else  
                {  
                    throw new FormatException($"'C' 选项在这里无效。(位置={i})");  
                }  
                var data = GetSet(type);  
                data.Add(val);  
                i++;  
                return i;  
            }

            if (c == '-')  
            {  
                i++;  
                c = s\[i\];  
                int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);  
                end = v;  
                i++;  
                if (i >= s.Length)  
                {  
                    AddToSet(val, end, 1, type);  
                    return i;  
                }  
                c = s\[i\];  
                if (c >= '0' && c <= '9')  
                {  
                    ValueSet vs = GetValue(v, s, i);  
                    int v1 = vs.theValue;  
                    end = v1;  
                    i = vs.pos;  
                }  
                if (i < s.Length && s\[i\] == '/')  
                {  
                    i++;  
                    c = s\[i\];  
                    int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);  
                    i++;  
                    if (i >= s.Length)  
                    {  
                        AddToSet(val, end, v2, type);  
                        return i;  
                    }  
                    c = s\[i\];  
                    if (c >= '0' && c <= '9')  
                    {  
                        ValueSet vs = GetValue(v2, s, i);  
                        int v3 = vs.theValue;  
                        AddToSet(val, end, v3, type);  
                        i = vs.pos;  
                        return i;  
                    }  
                    AddToSet(val, end, v2, type);  
                    return i;  
                }  
                AddToSet(val, end, 1, type);  
                return i;  
            }

            if (c == '/')  
            {  
                if (i + 1 >= s.Length || s\[i + 1\] == ' ' || s\[i + 1\] == '\\t')  
                {  
                    throw new FormatException("\\'/\\' 后面必须跟一个整数。");  
                }

                i++;  
                c = s\[i\];  
                int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);  
                i++;  
                if (i >= s.Length)  
                {  
                    CheckIncrementRange(v2, type);  
                    AddToSet(val, end, v2, type);  
                    return i;  
                }  
                c = s\[i\];  
                if (c >= '0' && c <= '9')  
                {  
                    ValueSet vs = GetValue(v2, s, i);  
                    int v3 = vs.theValue;  
                    CheckIncrementRange(v3, type);  
                    AddToSet(val, end, v3, type);  
                    i = vs.pos;  
                    return i;  
                }  
                throw new FormatException($"意外的字符 '{c}' 后 '/'");  
            }

            AddToSet(val, end, 0, type);  
            i++;  
            return i;  
        }

        /// <summary>  
        /// Gets the cron expression string.  
        /// </summary>  
        /// <value>The cron expression string.</value>  
        private static string CronExpressionString;

        /// <summary>  
        /// Skips the white space.  
        /// </summary>  
        /// <param name="i">The i.</param>  
        /// <param name="s">The s.</param>  
        /// <returns></returns>  
        private static int SkipWhiteSpace(int i, string s)  
        {  
            for (; i < s.Length && (s\[i\] == ' ' || s\[i\] == '\\t'); i++)  
            {  
            }

            return i;  
        }

        /// <summary>  
        /// Finds the next white space.  
        /// </summary>  
        /// <param name="i">The i.</param>  
        /// <param name="s">The s.</param>  
        /// <returns></returns>  
        private static int FindNextWhiteSpace(int i, string s)  
        {  
            for (; i < s.Length && (s\[i\] != ' ' || s\[i\] != '\\t'); i++)  
            {  
            }

            return i;  
        }

        /// <summary>  
        /// Adds to set.  
        /// </summary>  
        /// <param name="val">The val.</param>  
        /// <param name="end">The end.</param>  
        /// <param name="incr">The incr.</param>  
        /// <param name="type">The type.</param>  
        private void AddToSet(int val, int end, int incr, int type)  
        {  
            var data = GetSet(type);

            if (type == Second || type == Minute)  
            {  
                if ((val < 0 || val > 59 || end > 59) && val != AllSpecInt)  
                {  
                    throw new FormatException("分钟和秒值必须介于0和59之间");  
                }  
            }  
            else if (type == Hour)  
            {  
                if ((val < 0 || val > 23 || end > 23) && val != AllSpecInt)  
                {  
                    throw new FormatException("小时值必须介于0和23之间");  
                }  
            }  
            else if (type == DayOfMonth)  
            {  
                if ((val < 1 || val > 31 || end > 31) && val != AllSpecInt  
                    && val != NoSpecInt)  
                {  
                    throw new FormatException("月日值必须介于1和31之间");  
                }  
            }  
            else if (type == Month)  
            {  
                if ((val < 1 || val > 12 || end > 12) && val != AllSpecInt)  
                {  
                    throw new FormatException("月份值必须介于1和12之间");  
                }  
            }  
            else if (type == DayOfWeek)  
            {  
                if ((val == 0 || val > 7 || end > 7) && val != AllSpecInt  
                    && val != NoSpecInt)  
                {  
                    throw new FormatException("星期日值必须介于1和7之间");  
                }  
            }

            if ((incr == 0 || incr == -1) && val != AllSpecInt)  
            {  
                if (val != -1)  
                {  
                    data.Add(val);  
                }  
                else  
                {  
                    data.Add(NoSpec);  
                }  
                return;  
            }

            int startAt = val;  
            int stopAt = end;

            if (val == AllSpecInt && incr <= 0)  
            {  
                incr = 1;  
                data.Add(AllSpec);  
            }

            if (type == Second || type == Minute)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = 59;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 0;  
                }  
            }  
            else if (type == Hour)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = 23;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 0;  
                }  
            }  
            else if (type == DayOfMonth)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = 31;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 1;  
                }  
            }  
            else if (type == Month)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = 12;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 1;  
                }  
            }  
            else if (type == DayOfWeek)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = 7;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 1;  
                }  
            }  
            else if (type == Year)  
            {  
                if (stopAt == -1)  
                {  
                    stopAt = MaxYear;  
                }  
                if (startAt == -1 || startAt == AllSpecInt)  
                {  
                    startAt = 1970;  
                }  
            }

            int max = -1;  
            if (stopAt < startAt)  
            {  
                switch (type)  
                {  
                    case Second:  
                        max = 60;  
                        break;  
                    case Minute:  
                        max = 60;  
                        break;  
                    case Hour:  
                        max = 24;  
                        break;  
                    case Month:  
                        max = 12;  
                        break;  
                    case DayOfWeek:  
                        max = 7;  
                        break;  
                    case DayOfMonth:  
                        max = 31;  
                        break;  
                    case Year:  
                        throw new ArgumentException("开始年份必须小于停止年份");  
                    default:  
                        throw new ArgumentException("遇到意外的类型");  
                }  
                stopAt += max;  
            }

            for (int i = startAt; i <= stopAt; i += incr)  
            {  
                if (max == -1)  
                {  
                    data.Add(i);  
                }  
                else  
                {  
                    int i2 = i % max;  
                    if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))  
                    {  
                        i2 = max;  
                    }

                    data.Add(i2);  
                }  
            }  
        }

        /// <summary>  
        /// Gets the set of given type.  
        /// </summary>  
        /// <param name="type">The type of set to get.</param>  
        /// <returns></returns>  
        private SortedSet<int> GetSet(int type)  
        {  
            switch (type)  
            {  
                case Second:  
                    return seconds;  
                case Minute:  
                    return minutes;  
                case Hour:  
                    return hours;  
                case DayOfMonth:  
                    return daysOfMonth;  
                case Month:  
                    return months;  
                case DayOfWeek:  
                    return daysOfWeek;  
                case Year:  
                    return years;  
                default:  
                    throw new ArgumentOutOfRangeException();  
            }  
        }

        /// <summary>  
        /// Gets the value.  
        /// </summary>  
        /// <param name="v">The v.</param>  
        /// <param name="s">The s.</param>  
        /// <param name="i">The i.</param>  
        /// <returns></returns>  
        private static ValueSet GetValue(int v, string s, int i)  
        {  
            char c = s\[i\];  
            StringBuilder s1 = new StringBuilder(v.ToString(CultureInfo.InvariantCulture));  
            while (c >= '0' && c <= '9')  
            {  
                s1.Append(c);  
                i++;  
                if (i >= s.Length)  
                {  
                    break;  
                }  
                c = s\[i\];  
            }  
            ValueSet val = new ValueSet();  
            if (i < s.Length)  
            {  
                val.pos = i;  
            }  
            else  
            {  
                val.pos = i + 1;  
            }  
            val.theValue = Convert.ToInt32(s1.ToString(), CultureInfo.InvariantCulture);  
            return val;  
        }

        /// <summary>  
        /// Gets the numeric value from string.  
        /// </summary>  
        /// <param name="s">The string to parse from.</param>  
        /// <param name="i">The i.</param>  
        /// <returns></returns>  
        private static int GetNumericValue(string s, int i)  
        {  
            int endOfVal = FindNextWhiteSpace(i, s);  
            string val = s.Substring(i, endOfVal - i);  
            return Convert.ToInt32(val, CultureInfo.InvariantCulture);  
        }

        /// <summary>  
        /// Gets the month number.  
        /// </summary>  
        /// <param name="s">The string to map with.</param>  
        /// <returns></returns>  
        private static int GetMonthNumber(string s)  
        {  
            if (monthMap.ContainsKey(s))  
            {  
                return monthMap\[s\];  
            }

            return -1;  
        }

        /// <summary>  
        /// Gets the day of week number.  
        /// </summary>  
        /// <param name="s">The s.</param>  
        /// <returns></returns>  
        private static int GetDayOfWeekNumber(string s)  
        {  
            if (dayMap.ContainsKey(s))  
            {  
                return dayMap\[s\];  
            }

            return -1;  
        }

        /// <summary>  
        /// 在给定时间之后获取下一个触发时间。  
        /// </summary>  
        /// <param name="afterTimeUtc">开始搜索的 UTC 时间。</param>  
        /// <returns></returns>  
        public DateTimeOffset? GetTimeAfter(DateTimeOffset afterTimeUtc)  
        {

            // 向前移动一秒钟,因为我们正在计算时间\*之后\*  
            afterTimeUtc = afterTimeUtc.AddSeconds(1);

            // CronTrigger 不处理毫秒  
            DateTimeOffset d = CreateDateTimeWithoutMillis(afterTimeUtc);

            // 更改为指定时区  
            d = TimeZoneInfo.ConvertTime(d, timeZoneInfo);

            bool gotOne = false;  
            //循环直到我们计算出下一次,或者我们已经过了 endTime  
            while (!gotOne)  
            {  
                SortedSet<int> st;  
                int t;  
                int sec = d.Second;

                st = seconds.GetViewBetween(sec, 9999999);  
                if (st.Count > 0)  
                {  
                    sec = st.First();  
                }  
                else  
                {  
                    sec = seconds.First();  
                    d = d.AddMinutes(1);  
                }  
                d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond, d.Offset);

                int min = d.Minute;  
                int hr = d.Hour;  
                t = -1;

                st = minutes.GetViewBetween(min, 9999999);  
                if (st.Count > 0)  
                {  
                    t = min;  
                    min = st.First();  
                }  
                else  
                {  
                    min = minutes.First();  
                    hr++;  
                }  
                if (min != t)  
                {  
                    d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond, d.Offset);  
                    d = SetCalendarHour(d, hr);  
                    continue;  
                }  
                d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond, d.Offset);

                hr = d.Hour;  
                int day = d.Day;  
                t = -1;

                st = hours.GetViewBetween(hr, 9999999);  
                if (st.Count > 0)  
                {  
                    t = hr;  
                    hr = st.First();  
                }  
                else  
                {  
                    hr = hours.First();  
                    day++;  
                }  
                if (hr != t)  
                {  
                    int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);  
                    if (day > daysInMonth)  
                    {  
                        d = new DateTimeOffset(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond, d.Offset).AddDays(day - daysInMonth);  
                    }  
                    else  
                    {  
                        d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond, d.Offset);  
                    }  
                    d = SetCalendarHour(d, hr);  
                    continue;  
                }  
                d = new DateTimeOffset(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond, d.Offset);

                day = d.Day;  
                int mon = d.Month;  
                t = -1;  
                int tmon = mon;

                bool dayOfMSpec = !daysOfMonth.Contains(NoSpec);  
                bool dayOfWSpec = !daysOfWeek.Contains(NoSpec);  
                if (dayOfMSpec && !dayOfWSpec)  
                {  
                    // 逐月获取规则  
                    st = daysOfMonth.GetViewBetween(day, 9999999);  
                    bool found = st.Any();  
                    if (lastdayOfMonth)  
                    {  
                        if (!nearestWeekday)  
                        {  
                            t = day;  
                            day = GetLastDayOfMonth(mon, d.Year);  
                            day -= lastdayOffset;

                            if (t > day)  
                            {  
                                mon++;  
                                if (mon > 12)  
                                {  
                                    mon = 1;  
                                    tmon = 3333; // 确保下面的 mon != tmon 测试失败  
                                    d = d.AddYears(1);  
                                }  
                                day = 1;  
                            }  
                        }  
                        else  
                        {  
                            t = day;  
                            day = GetLastDayOfMonth(mon, d.Year);  
                            day -= lastdayOffset;

                            DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);

                            int ldom = GetLastDayOfMonth(mon, d.Year);  
                            DayOfWeek dow = tcal.DayOfWeek;

                            if (dow == System.DayOfWeek.Saturday && day == 1)  
                            {  
                                day += 2;  
                            }  
                            else if (dow == System.DayOfWeek.Saturday)  
                            {  
                                day -= 1;  
                            }  
                            else if (dow == System.DayOfWeek.Sunday && day == ldom)  
                            {  
                                day -= 2;  
                            }  
                            else if (dow == System.DayOfWeek.Sunday)  
                            {  
                                day += 1;  
                            }

                            DateTimeOffset nTime = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Millisecond, d.Offset);  
                            if (nTime.ToUniversalTime() < afterTimeUtc)  
                            {  
                                day = 1;  
                                mon++;  
                            }  
                        }  
                    }  
                    else if (nearestWeekday)  
                    {  
                        t = day;  
                        day = daysOfMonth.First();

                        DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);

                        int ldom = GetLastDayOfMonth(mon, d.Year);  
                        DayOfWeek dow = tcal.DayOfWeek;

                        if (dow == System.DayOfWeek.Saturday && day == 1)  
                        {  
                            day += 2;  
                        }  
                        else if (dow == System.DayOfWeek.Saturday)  
                        {  
                            day -= 1;  
                        }  
                        else if (dow == System.DayOfWeek.Sunday && day == ldom)  
                        {  
                            day -= 2;  
                        }  
                        else if (dow == System.DayOfWeek.Sunday)  
                        {  
                            day += 1;  
                        }

                        tcal = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Offset);  
                        if (tcal.ToUniversalTime() < afterTimeUtc)  
                        {  
                            day = daysOfMonth.First();  
                            mon++;  
                        }  
                    }  
                    else if (found)  
                    {  
                        t = day;  
                        day = st.First();

                        //确保我们不会在短时间内跑得过快,比如二月  
                        int lastDay = GetLastDayOfMonth(mon, d.Year);  
                        if (day > lastDay)  
                        {  
                            day = daysOfMonth.First();  
                            mon++;  
                        }  
                    }  
                    else  
                    {  
                        day = daysOfMonth.First();  
                        mon++;  
                    }

                    if (day != t || mon != tmon)  
                    {  
                        if (mon > 12)  
                        {  
                            d = new DateTimeOffset(d.Year, 12, day, 0, 0, 0, d.Offset).AddMonths(mon - 12);  
                        }  
                        else  
                        {  
                            //这是为了避免从一个月移动时出现错误  
                            //有 30 或 31 天到一个月更少。 导致实例化无效的日期时间。  
                            int lDay = DateTime.DaysInMonth(d.Year, mon);  
                            if (day <= lDay)  
                            {  
                                d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);  
                            }  
                            else  
                            {  
                                d = new DateTimeOffset(d.Year, mon, lDay, 0, 0, 0, d.Offset).AddDays(day - lDay);  
                            }  
                        }  
                        continue;  
                    }  
                }  
                else if (dayOfWSpec && !dayOfMSpec)  
                {  
                    // 获取星期几规则  
                    if (lastdayOfWeek)  
                    {

                        int dow = daysOfWeek.First();

                        int cDow = (int)d.DayOfWeek + 1;  
                        int daysToAdd = 0;  
                        if (cDow < dow)  
                        {  
                            daysToAdd = dow - cDow;  
                        }  
                        if (cDow > dow)  
                        {  
                            daysToAdd = dow + (7 - cDow);  
                        }

                        int lDay = GetLastDayOfMonth(mon, d.Year);

                        if (day + daysToAdd > lDay)  
                        {

                            if (mon == 12)  
                            {

                                d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);  
                            }  
                            else  
                            {  
                                d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);  
                            }

                            continue;  
                        }

                        // 查找本月这一天最后一次出现的日期...  
                        while (day + daysToAdd + 7 <= lDay)  
                        {  
                            daysToAdd += 7;  
                        }

                        day += daysToAdd;

                        if (daysToAdd > 0)  
                        {  
                            d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);

                            continue;  
                        }  
                    }  
                    else if (nthdayOfWeek != 0)  
                    {

                        int dow = daysOfWeek.First();

                        int cDow = (int)d.DayOfWeek + 1;  
                        int daysToAdd = 0;  
                        if (cDow < dow)  
                        {  
                            daysToAdd = dow - cDow;  
                        }  
                        else if (cDow > dow)  
                        {  
                            daysToAdd = dow + (7 - cDow);  
                        }

                        bool dayShifted = daysToAdd > 0;

                        day += daysToAdd;  
                        int weekOfMonth = day / 7;  
                        if (day % 7 > 0)  
                        {  
                            weekOfMonth++;  
                        }

                        daysToAdd = (nthdayOfWeek - weekOfMonth) \* 7;  
                        day += daysToAdd;  
                        if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year))  
                        {  
                            if (mon == 12)  
                            {  
                                d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);  
                            }  
                            else  
                            {  
                                d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);  
                            }

                            continue;  
                        }  
                        if (daysToAdd > 0 || dayShifted)  
                        {  
                            d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);

                            continue;  
                        }  
                    }  
                    else if (everyNthWeek != 0)  
                    {  
                        int cDow = (int)d.DayOfWeek + 1;  
                        int dow = daysOfWeek.First();

                        st = daysOfWeek.GetViewBetween(cDow, 9999999);  
                        if (st.Count > 0)  
                        {  
                            dow = st.First();  
                        }

                        int daysToAdd = 0;  
                        if (cDow < dow)  
                        {  
                            daysToAdd = (dow - cDow) + (7 \* (everyNthWeek - 1));  
                        }  
                        if (cDow > dow)  
                        {  
                            daysToAdd = (dow + (7 - cDow)) + (7 \* (everyNthWeek - 1));  
                        }

                        if (daysToAdd > 0)  
                        {  
                            d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);  
                            d = d.AddDays(daysToAdd);  
                            continue;  
                        }  
                    }  
                    else  
                    {  
                        int cDow = (int)d.DayOfWeek + 1;  
                        int dow = daysOfWeek.First();

                        st = daysOfWeek.GetViewBetween(cDow, 9999999);  
                        if (st.Count > 0)  
                        {  
                            dow = st.First();  
                        }

                        int daysToAdd = 0;  
                        if (cDow < dow)  
                        {  
                            daysToAdd = dow - cDow;  
                        }  
                        if (cDow > dow)  
                        {  
                            daysToAdd = dow + (7 - cDow);  
                        }

                        int lDay = GetLastDayOfMonth(mon, d.Year);

                        if (day + daysToAdd > lDay)  
                        {

                            if (mon == 12)  
                            {  
                                d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);  
                            }  
                            else  
                            {  
                                d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);  
                            }  
                            continue;  
                        }  
                        if (daysToAdd > 0)  
                        {  
                            d = new DateTimeOffset(d.Year, mon, day + daysToAdd, 0, 0, 0, d.Offset);  
                            continue;  
                        }  
                    }  
                }  
                else  
                {  
                    throw new FormatException("不支持同时指定星期日和月日参数。");  
                }

                d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, d.Minute, d.Second, d.Offset);  
                mon = d.Month;  
                int year = d.Year;  
                t = -1;

                if (year > MaxYear)  
                {  
                    return null;  
                }

                st = months.GetViewBetween(mon, 9999999);  
                if (st.Count > 0)  
                {  
                    t = mon;  
                    mon = st.First();  
                }  
                else  
                {  
                    mon = months.First();  
                    year++;  
                }  
                if (mon != t)  
                {  
                    d = new DateTimeOffset(year, mon, 1, 0, 0, 0, d.Offset);  
                    continue;  
                }  
                d = new DateTimeOffset(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second, d.Offset);  
                year = d.Year;  
                t = -1;

                st = years.GetViewBetween(year, 9999999);  
                if (st.Count > 0)  
                {  
                    t = year;  
                    year = st.First();  
                }  
                else  
                {  
                    return null;  
                }

                if (year != t)  
                {  
                    d = new DateTimeOffset(year, 1, 1, 0, 0, 0, d.Offset);  
                    continue;  
                }  
                d = new DateTimeOffset(year, d.Month, d.Day, d.Hour, d.Minute, d.Second, d.Offset);

                //为此日期应用适当的偏移量  
                d = new DateTimeOffset(d.DateTime, timeZoneInfo.BaseUtcOffset);

                gotOne = true;  
            }

            return d.ToUniversalTime();  
        }

        /// <summary>  
        /// Creates the date time without milliseconds.  
        /// </summary>  
        /// <param name="time">The time.</param>  
        /// <returns></returns>  
        private static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time)  
        {  
            return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset);  
        }

        /// <summary>  
        /// Advance the calendar to the particular hour paying particular attention  
        /// to daylight saving problems.  
        /// </summary>  
        /// <param name="date">The date.</param>  
        /// <param name="hour">The hour.</param>  
        /// <returns></returns>  
        private static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour)  
        {

            int hourToSet = hour;  
            if (hourToSet == 24)  
            {  
                hourToSet = 0;  
            }  
            DateTimeOffset d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset);  
            if (hour == 24)  
            {  
                d = d.AddDays(1);  
            }  
            return d;  
        }

        /// <summary>  
        /// Gets the last day of month.  
        /// </summary>  
        /// <param name="monthNum">The month num.</param>  
        /// <param name="year">The year.</param>  
        /// <returns></returns>  
        private static int GetLastDayOfMonth(int monthNum, int year)  
        {  
            return DateTime.DaysInMonth(year, monthNum);  
        }

        private class ValueSet  
        {  
            public int theValue;

            public int pos;  
        }

    }

}

}

CronHelper 中 CronExpression 的函数计算逻辑是从 Quart.NET 借鉴的,支持标准的 7位 cron 表达式,在需要生成Cron 表达式时可以直接使用网络上的各种 Cron 表达式在线生成

CronHelper 里面我们主要用到的功能就是 通过 Cron 表达式,解析下一次的执行时间。

服务运行这块我们采用微软的 BackgroundService 后台服务,这里还要用到一个后台服务批量注入的逻辑 关于后台逻辑批量注入可以看我之前写的一篇博客,这里就不展开介绍了

.NET 使用自带 DI 批量注入服务(Service)和 后台服务(BackgroundService) https://www.cnblogs.com/berkerdong/p/16496232.html

接下来看一下我这里写的一个DemoTask,代码如下:

using DistributedLock;
using Repository.Database;
using TaskService.Libraries;

namespace TaskService.Tasks
{
public class DemoTask : BackgroundService
{

    private readonly IServiceProvider serviceProvider;  
    private readonly ILogger logger;

    public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger)  
    {  
        this.serviceProvider = serviceProvider;  
        this.logger = logger;  
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        CronSchedule.BatchBuilder(stoppingToken, this);

        await Task.Delay(-1, stoppingToken);  
    }

    \[CronSchedule(Cron = "0/1 \* \* \* \* ?")\]  
    public void ClearLog()  
    {  
        try  
        {  
            using var scope = serviceProvider.CreateScope();  
            var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();

            //省略业务代码  
            Console.WriteLine("ClearLog:" + DateTime.Now);  
        }  
        catch (Exception ex)  
        {  
            logger.LogError(ex, "DemoTask.ClearLog");  
        }  
    }

    \[CronSchedule(Cron = "0/5 \* \* \* \* ?")\]  
    public void ClearCache()  
    {  
        try  
        {  
            using var scope = serviceProvider.CreateScope();  
            var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();  
            var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();

            //省略业务代码  
            Console.WriteLine("ClearCache:" + DateTime.Now);  
        }  
        catch (Exception ex)  
        {  
            logger.LogError(ex, "DemoTask.ClearCache");  
        }  
    }

}  

}

该Task中有两个方法 ClearLog 和 ClearCache 他们分别会每1秒和每5秒执行一次。需要注意在后台服务中对于 Scope 生命周期的服务在获取是需要手动 CreateScope();

实现的关键点在于 服务执行 ExecuteAsync 中的 CronSchedule.BatchBuilder(stoppingToken, this); 我们这里将代码有 CronSchedule 标记头的方法全部循环进行了启动,该方法的代码如下:

using Common;
using System.Reflection;

namespace TaskService.Libraries
{
public class CronSchedule
{
public static void BatchBuilder(CancellationToken stoppingToken, object context)
{
var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList();

        foreach (var t in taskList)  
        {  
            string cron = t.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!;

            Builder(stoppingToken, cron, t, context);  
        }  
    }

    private static async void Builder(CancellationToken stoppingToken, string cronExpression, MethodInfo action, object context)  
    {  
        var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));

        while (!stoppingToken.IsCancellationRequested)  
        {  
            var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));

            if (nextTime == nowTime)  
            {  
                \_ = Task.Run(() =>  
                {  
                    action.Invoke(context, null);

                });

                nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));  
            }  
            else if (nextTime < nowTime)  
            {  
                nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));  
            }

            await Task.Delay(1000, stoppingToken);  
        }  
    }

}

\[AttributeUsage(AttributeTargets.Method)\]  
public class CronScheduleAttribute : Attribute  
{  
    public string Cron { get; set; }

}  

}

主要就是利用反射获取当前类中所有带有 CronSchedule 标记的方法,然后解析对应的 Cron 表达式获取下一次的执行时间,如果执行时间等于当前时间则执行一次方法,否则等待1秒钟循环重复这个逻辑。

然后启动我们的项目就可以看到如下的运行效果:

ClearLog 每1秒钟执行一次,ClearCache 每 5秒钟执行一次

至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下

https://github.com/berkerdong/NetEngine.git

https://gitee.com/berkerdong/NetEngine.git