常用的定时任务组件有 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 基础框架项目,项目地址如下

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件的更多相关文章

  1. .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版)

    在上个月写过一篇 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 的文章,当时 CronSchedule 的实现是使用了,每个服务都独立进入到一个 while 循环中,进行定期扫描是否 ...

  2. .NET定时任务执行管理器开源组件–FluentScheduler

    在日常项目里通常会遇到定时执行任务的需求,也就是定时器..NET Framework里关于定时器的类有3个,分别是System.Windows.Forms.Timer.System.Timers.Ti ...

  3. linux ,cron定时任务 备份mysql数据库

    cron 定时任务执行备份脚本文件 backup.sh #!/bin/bash USER="root" PASSWORD="xxxxx" DATABASE=&q ...

  4. Go cron定时任务的用法

    cron是什么 cron的意思就是:计划任务,说白了就是定时任务.我和系统约个时间,你在几点几分几秒或者每隔几分钟跑一个任务(job),就那么简单. cron表达式 cron表达式是一个好东西,这个东 ...

  5. spring cron 定时任务

    文章首发于个人博客:https://yeyouluo.github.io 0 预备知识:cron表达式 见 <5 参考>一节. 1 环境 eclipse mars2 + Maven3.3. ...

  6. Spring task定时任务执行一段时间后莫名其妙停止的问题

    前因: 我写了一个小项目,主要功能是用Spring task定时任务每天定时给用户发送邮件.执行了几个月一直没有问题,前几天,莫名其妙的突然不再发送邮件了. 只好花费一些时间来查看到底是什么原因造成的 ...

  7. cron定时任务介绍

    什么是cron? Cron是linux系统中用来定期执行或指定程序任务的一种服务或软件.与它相关的有两个工具:crond 和 crontab.crond 就是 cron 在系统内的宿主程序,cront ...

  8. spring task 实现定时执行(补充:解决定时任务执行2次问题)

    首先在spring-mvc.xml配置头文件引入: xmlns:task="http://www.springframework.org/schema/task" 其次引入task ...

  9. linux之cron定时任务介绍

    前言 linux系统有一个专门用来管理定时任务的进程cron,一般是设置成开机自启动的,通过添加任务可以让服务器定时执行某些任务. cron介绍 linux系统有一个专门用来管理定时任务的进程cron ...

随机推荐

  1. 『忘了再学』Shell流程控制 — 33、if条件判断语句(一)

    目录 1.单分支if条件语句 2.双分支if条件语句 (1)示例1 (2)示例2 什么是流程控制? 普通理解:Shell编写的程序是顺序执行的,也就是说第一命令先执行,然后接着执行第二条命令,然后再下 ...

  2. BUUCTF-ningen

    ningen 从16进制看可以发现其中有压缩包,存在着504b0304,使用binwalk分离即可 压缩包带密码,根据提示是四位纯数字 使用ARCHPR破解即可

  3. 微信0day复现

    由于微信采用的是google内核,前些日子google爆出0day远程代码执行漏洞,但是需要关闭沙箱,而微信采用的是老版本google内核,默认关闭沙箱,因此只要微信用户点击恶意连接,可直接获取该PC ...

  4. SAP OOALV- 合计

    TYPES: BEGIN OF ty_mara, srno LIKE adrc-name1, " Storing the total text matnr LIKE mara-matnr, ...

  5. Node.js精进(5)——HTTP

    HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...

  6. bat-注册表修改win11右键风格

    展开:reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32&q ...

  7. 腾讯云原生数据库TDSQL-C架构探索和实践

    作为云原生技术先驱,腾讯云数据库内核团队致力于不断提升产品的可用性.可靠性.性能和可扩展性,为用户提供更加极致的体验.为帮助用户了解极致体验背后的关键技术点,本期带来腾讯云数据库专家工程师王鲁俊给大家 ...

  8. File类创建删除功能的方法和File类遍历(文件夹)目录功能

    File类创建删除功能的方法 -public boolean createNewFile():当且仅当具有该名称的文件尚不存在时,创建一个新的空文件 -public boolean delete(): ...

  9. HashMap存储自定义类型键值和LinkedHashMap集合

    HashMap存储自定义类型键值 1.当给HashMap中存放自定义对象时,如果自定义对象是键存在,保证键唯一,必须复写对象的hashCode和equals方法. 2.如果要保证map中存放的key和 ...

  10. ASP.NET MVC-动态网页开发-宿舍管理系统

    很不容易,我在这两周为了数据库的课程设计第一次学习到了动态网页的开发.首先是尊重知识,也是为了知识不被忘记,在这里写下这第一篇博客.才疏学浅如果有什么理解错误,多包涵. 首先是环境的配置,我自己使用的 ...