For services that I write that repeat a task over a set interval, I generally use a simple repeater loop. For the cases where I needed a bit more granular control of how/when Tasks repeat, I created a simple scheduler with a Fluent API.
Basically, you can schedule Actions (which are executed through a Task) to be executed on a regular basis. For simple repeating tasks, it supports running every X minutes, X seconds, X milliseconds. Any interval/length of time more than that on a simple repeat would be some multiple of seconds/milliseconds/minutes.
It supports a start date and start time as DateTime and TimeSpan, running once a month, running only on certain days of the week (with the “RunMonthly”, it will expect to run on a single day), running daily, etc etc.
Here’s an example of a task/thread to be executed once a day at 3:15AM would look like, for example:
var schedule3 = new TaskSchedule() .WithName("DailySchedule All Days @ 3:15 AM") .RepeatDaily() .WithStartDate(new DateTime(2016, 9, 1)) .WithStartTime(new TimeSpan(3, 15, 0)) // Run at 03:15 AM .WithAction(() => { Console.WriteLine(); Console.WriteLine("I'm your action3. {0:MM/dd/yyyy HH:mm:ss.fff}", TaskScheduleTimer.UtcNow.ToLocalTime()); }, cancellationTokenSource.Token);
Here’s what a task/thread to be executed once a day but only on certain days would look like:
var schedule4 = new TaskSchedule() .WithName("DailySchedule Days Sat, Sun, Wed @ 6PM") .RepeatDaily() .WithStartDate(new DateTime(2016, 9, 3)) .WithStartTime(new TimeSpan(18, 0, 0)) .WithDaysOfWeek(new List<DayOfWeek>() { DayOfWeek.Saturday, DayOfWeek.Sunday, DayOfWeek.Wednesday }) .WithAction(() => { Console.WriteLine(); Console.WriteLine("Here's some action4. {0:MM/dd/yyyy HH:mm:ss.fff}", DateTime.UtcNow.ToLocalTime()); }, cancellationTokenSource.Token);
Full source code is below, and I also put this into a Gist. You’ll find it’s relatively simple, while the bulk of code comprises the fluent interface.
/// <summary> /// Simple repeater based on a timed interval /// </summary> public static class TaskRepeater { public static Task Interval(TimeSpan pollInterval, Action action, CancellationToken token, bool runImmediately = false) { // We don't use Observable.Interval: // If we block, the values start bunching up behind each other. return Task.Factory.StartNew( () => { if (runImmediately) { for (;;) { action(); if (token.WaitCancellationRequested(pollInterval)) break; } } else { for (;;) { if (token.WaitCancellationRequested(pollInterval)) break; action(); } } }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } } /// <summary> /// Task scheduler with many options and FluentAPI /// </summary> public class TaskSchedule { private int _repeatMilliseconds = 0; private bool _repeatDaily = false; private bool _repeatMonthly = false; private string _name = "NoName"; private List<DayOfWeek> _daysOfWeek = new List<DayOfWeek>() { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday }; private DateTime _lastRun = DateTime.UtcNow.AddDays(-1); private DateTime _nextRun = DateTime.UtcNow.AddDays(-1); private DateTime _startDate = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1); private TimeSpan _startTime = new TimeSpan(0, 0, 0); private bool _acceleratedTime = false; private Action _action; private CancellationToken _token; public DateTime NextRun { get { return _nextRun; } set { _nextRun = value; } } public DateTime LastRun { get { return _lastRun; } set { _lastRun = value; } } public int RepeatMilliseconds { get { return _repeatMilliseconds; } set { _repeatMilliseconds = value; } } public bool RepeatDaily { get { return _repeatDaily; } set { _repeatDaily = value; _lastRun = _startDate + _startTime; } } public bool RepeatMonthly { get { return _repeatMonthly; } set { _repeatMonthly = value; _lastRun = _startDate + _startTime; } } public string Name { get { return _name; } set { _name = value; } } public List<DayOfWeek> DaysOfWeek { get { return _daysOfWeek; } set { _daysOfWeek = value; } } public DateTime StartDate { get { return _startDate; } set { _startDate = value; _lastRun = _startDate + _startTime; } } public TimeSpan StartTime { get { return _startTime; } set { _startTime = value; _lastRun = _startDate + _startTime; } } public bool AcceleratedTime { get { return _acceleratedTime; } set { _acceleratedTime = value; } } public Action ScheduleAction { get { return _action; } set { _action = value; } } public CancellationToken ScheduleToken { get { return _token; } set { _token = value; } } public TaskSchedule() { } public DateTime GetNextRun(DateTime? dateTime = null) { var currentDateTime = dateTime ?? TaskScheduleTimer.UtcNow; var testDate = currentDateTime.Date; if (_nextRun == DateTime.MinValue) { _lastRun = _nextRun = testDate = currentDateTime.AddMilliseconds(1); return testDate; } if (_repeatDaily) { testDate = testDate + _startTime; if (testDate < currentDateTime) { do { testDate = testDate.AddDays(1); } while (!_daysOfWeek.Any(x => x == testDate.DayOfWeek) && _daysOfWeek != null && _daysOfWeek.Count > 0); } } else if (_repeatMonthly) { testDate = new DateTime(testDate.Year, testDate.Month, _startDate.Date.Day) + _startTime; if (testDate < currentDateTime) { testDate = testDate.AddMonths(1); testDate = new DateTime(testDate.Year, testDate.Month, _startDate.Date.Day) + _startTime; while (!_daysOfWeek.Any(x => x == testDate.DayOfWeek) && _daysOfWeek != null && _daysOfWeek.Count > 0) { testDate = testDate.AddDays(1); } } } else { _lastRun = _nextRun; testDate = _lastRun; do { testDate = testDate.AddMilliseconds(_repeatMilliseconds); } while (testDate < currentDateTime); } _nextRun = testDate; return testDate; } public Task CreateTask() { return Task.Factory.StartNew(() => { TimeSpan pollInterval; DateTime nextRunDate; for (;;) { var currentDateTime = TaskScheduleTimer.UtcNow; nextRunDate = this.GetNextRun(currentDateTime); pollInterval = nextRunDate - currentDateTime; if (_acceleratedTime) { pollInterval = TimeSpan.FromMilliseconds(500); } Console.WriteLine(string.Format("[* {0} *]: Sleeping until {1:MM/dd/yyyy HH:mm:ss.fff}, Interval: {2}", _name, nextRunDate.ToLocalTime(), pollInterval)); // We have to chunk the wait if we exceed Int32.MaxValue var totalMilliseconds = pollInterval.TotalMilliseconds; if (totalMilliseconds <= int.MaxValue) { if (_token.WaitCancellationRequested(pollInterval)) break; } else { while (totalMilliseconds > 0 && !_token.IsCancellationRequested) { var currentDelay = totalMilliseconds > int.MaxValue ? int.MaxValue : (int)totalMilliseconds; if (_token.WaitCancellationRequested(TimeSpan.FromMilliseconds(currentDelay))) break; totalMilliseconds -= currentDelay; } if (_token.IsCancellationRequested) { break; } } _action(); if (_acceleratedTime) { TaskScheduleTimer.SetCurrent(nextRunDate); } } if (_token.IsCancellationRequested) { Console.WriteLine(string.Format("[* {0} *]: Cancelled", _name)); } }, _token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } } public class TaskScheduleTimer { private static DateTime _startTime; private static Stopwatch _stopWatch = null; private static TimeSpan _maxIdle = TimeSpan.FromSeconds(10); public static DateTime UtcNow { get { if ((_stopWatch == null) || (_startTime.Add(_maxIdle) < DateTime.UtcNow)) { Reset(); } return _startTime.AddTicks(_stopWatch.Elapsed.Ticks); } } public static void SetCurrent(DateTime dateTime) { _startTime = dateTime; } private static void Reset() { _startTime = DateTime.UtcNow; _stopWatch = Stopwatch.StartNew(); } public static Stopwatch Stopwatch { get { return _stopWatch; } } } public static class Extensions { public static TaskSchedule WithName(this TaskSchedule schedule, string name) { schedule.Name = name; return schedule; } public static TaskSchedule RepeatMilliseconds(this TaskSchedule schedule, int milliseconds) { schedule.RepeatMilliseconds = milliseconds; return schedule; } public static TaskSchedule RepeatSeconds(this TaskSchedule schedule, int seconds) { schedule.RepeatMilliseconds = seconds * 1000; return schedule; } public static TaskSchedule RepeatMinutes(this TaskSchedule schedule, int minutes) { schedule.RepeatMilliseconds = minutes * 60000; return schedule; } public static TaskSchedule WithRunImmediately(this TaskSchedule schedule) { schedule.NextRun = DateTime.MinValue; return schedule; } public static TaskSchedule RepeatDaily(this TaskSchedule schedule, bool repeatDaily = true) { schedule.RepeatDaily = repeatDaily; schedule.RepeatMonthly = false; return schedule; } public static TaskSchedule RepeatMonthly(this TaskSchedule schedule, bool repeatMonthly = true) { schedule.RepeatMonthly = repeatMonthly; schedule.RepeatDaily = false; return schedule; } public static TaskSchedule WithDaysOfWeek(this TaskSchedule schedule, List<DayOfWeek> daysOfWeek) { schedule.DaysOfWeek = daysOfWeek; return schedule; } public static TaskSchedule WithStartDate(this TaskSchedule schedule, DateTime startDate) { schedule.StartDate = startDate.Date >= DateTime.UtcNow.Date ? startDate : DateTime.UtcNow.Date; return schedule; } public static TaskSchedule WithStartTime(this TaskSchedule schedule, TimeSpan startTime) { schedule.StartTime = startTime; if (schedule.RepeatMilliseconds > 0) { schedule.NextRun = schedule.StartDate + schedule.StartTime; }; return schedule; } public static TaskSchedule WithAcceleratedTime(this TaskSchedule schedule) { schedule.AcceleratedTime = true; return schedule; } public static TaskSchedule WithAction(this TaskSchedule schedule, Action action, CancellationToken token) { schedule.ScheduleAction = action; schedule.ScheduleToken = token; schedule.CreateTask(); return schedule; } public static bool WaitCancellationRequested(this CancellationToken token, TimeSpan timeout) { return token.WaitHandle.WaitOne(timeout); } }