.NET 純原生實現 Cron 定時任務執行,未依賴第三方組件

来源:https://www.cnblogs.com/berkerdong/archive/2022/08/24/16619415.html
-Advertisement-
Play Games

常用的定時任務組件有 Quartz.Net 和 Hangfire 兩種,這兩種是使用人數比較多的定時任務組件,個人以前也是使用的 Hangfire ,慢慢的發現自己想要的其實只是一個能夠根據 Cron 表達式來定時執行函數的功能,Quartz.Net 和 Hangfire 雖然都能實現這個目的,但是 ...


常用的定時任務組件有 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
              
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 概述 本文描述WPF的拖放功能(Drag and Drop)。 拖放功能涉及到兩個功能,一個就是拖,一個是放。拖放可以發生在兩個控制項之間,也可以在一個控制項自己內部拖放。假設界面上有兩個控制項,一個TreeView,一個ListView,那麼可能發生的拖動有以下幾種情況: 1、TreeView -> L ...
  • 一文瞭解.Net的CLR、GC記憶體管理 微軟官方文檔對記憶體管理和CLR的概述 什麼是托管代碼? 托管代碼就是執行過程交由運行時管理的代碼。 在這種情況下,相關的運行時稱為公共語言運行時 (CLR),不管使用的是哪種實現(例如 Mono、.NET Framework 或 .NET Core/.NET ...
  • In a certain project, I need to calculate the altitude of the current location given the current location temperature and current location pressure. T... ...
  • 一:背景 去年 GC架構師 Maoni 在 (2021 .NET 開發者大會) [https://ke.segmentfault.com/course/1650000041122988/section/1500000041123017] 上演示過 PerfView 的 Diff 功能來尋找記憶體增量, ...
  • 什麼?WPF 不支持 SVG ? 控制項名:SharpVectors 作者:Elinam LLC (Japan) 項目地址: https://github.com/ElinamLLC/SharpVectors 什麼是SVG? SVG 指可伸縮矢量圖形 (Scalable Vector Graphics ...
  • wwm.LeetCodeHelper 倉庫地址:https://gitee.com/wwmin/www.leetcode.helper 1. 說明 wwm.LeetCodeHelper是一款幫助在本地用C#做LeetCode題的一個庫,具有自動拉取題生成csharp文件,自動生成測試用例,自動完成測 ...
  • 我是做11年軟體開發的架構師,想分享跟幹活給到大家。 如果你是剛開始學的大學生,可以看我教學視頻,滿滿的乾貨,幫助你快速找到滿意的工作。也可以通過真實的項目賺點錢。 如果你是剛開始入門的程式員,可以跟我學如何優化代碼,如何搭建框架。快速的學會如何搭建框架。滿足你的財富自由。 源代碼QQ群:43347 ...
  • 快速認識ORM 對象-關係映射,即Object/Relation Mapping,主要實現程式對象到關係資料庫的映射。現在.Net比較流行的ORM框架有:EF、SqlSugar、Dapper、FreeSql、Nhibernate、IBatis.Net等。 O/RM只是一層代碼的封裝,底層還是基於AD ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...