AFAIK there is nothing like this available in the standard .NET libraries. And I don't think that it's likely to be added any time soon. My suggestion is to use the third party Cronos library, that does a good job at calculating time intervals¹. You can find a usage example here, by Stephen Cleary. What this library does is to take a DateTime
and a Cron expression as input, and calculate the next DateTime
that satisfies this expression. It is just a DateTime
calculator, not a scheduler.
If you want to get fancy you could include the functionality of the Cronos library in a custom PeriodicTimer
-like component, like the one below:
using Cronos;
public sealed class CronosPeriodicTimer : IDisposable
{
private readonly CronExpression _cronExpression; // Also used as the locker
private PeriodicTimer _activeTimer;
private bool _disposed;
private static readonly TimeSpan _minDelay = TimeSpan.FromMilliseconds(500);
public CronosPeriodicTimer(string expression, CronFormat format)
{
_cronExpression = CronExpression.Parse(expression, format);
}
public async ValueTask<bool> WaitForNextTickAsync(
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
PeriodicTimer timer;
lock (_cronExpression)
{
if (_disposed) return false;
if (_activeTimer is not null)
throw new InvalidOperationException("One consumer at a time.");
DateTime utcNow = DateTime.UtcNow;
DateTime? utcNext = _cronExpression.GetNextOccurrence(utcNow + _minDelay);
if (utcNext is null)
throw new InvalidOperationException("Unreachable date.");
TimeSpan delay = utcNext.Value - utcNow;
Debug.Assert(delay > _minDelay);
timer = _activeTimer = new(delay);
}
try
{
// Dispose the timer after the first tick.
using (timer)
return await timer.WaitForNextTickAsync(cancellationToken)
.ConfigureAwait(false);
}
finally { Volatile.Write(ref _activeTimer, null); }
}
public void Dispose()
{
PeriodicTimer activeTimer;
lock (_cronExpression)
{
if (_disposed) return;
_disposed = true;
activeTimer = _activeTimer;
}
activeTimer?.Dispose();
}
}
Apart from the constructor, the CronosPeriodicTimer
class has identical API and behavior with the PeriodicTimer
class. You could use it like this:
var timer = new CronosPeriodicTimer("0 * * * * *", CronFormat.IncludeSeconds);
//...
await timer.WaitForNextTickAsync();
The expression 0 * * * * *
means "on the 0 (zero) second of every minute, of every hour, of every day of the month, of every month, and of every day of the week."
You can find detailed documentation about the format of the Cron expressions here.
The 500 milliseconds _minDelay
has the intention to prevent the remote possibility of the timer ticking twice by mistake. Also because the PeriodicTimer
class has a minimum period of 1 millisecond.
For an implementation that uses the Task.Delay
method instead of the PeriodicTimer
class, and so it can be used by .NET versions previous than 6.0, you can look at the 3rd revision of this answer.
¹ With the caveat that the Cronos library is currently capped to the year 2099 (version 0.7.1).