8 min read

The Power Of .Net Timers

.Net/Coding/Code/Web Development/Engineers/Coders/Developers/Scheduling/Automation/ML/AI?Artificial Intelligence/Machine Learning
Still using external service providers for scheduling requirements in applications? .Net timers can give you the same capabilities without the added overhead. Let's create an example.
Written by
Basant K
Published on
October 20, 2023

Need for a Custom Scheduler

Frequently, we default to external service providers for scheduling requirements in our applications. However, the majority of the time, these introduce an additional overhead of maintenance and administration. Our selections typically gravitate toward the following services:  

  1. Quartz.NET
  1. HangFire
  1. Azure Logic Apps

Considering 'Azure Logic Apps', for instance, it necessitates the maintenance of a unique logic app for each scheduling need. This becomes considerably painstaking when numerous environments are involved. One of the key limitations here is that our scheduler is positioned externally to the source code, and unfortunately, Azure Logic Apps do not offer version control features.  

In the case of alternative options such as the Quartz and HangFire libraries, one might question the necessity of delving into the intricacies of their specific syntax for the simple task of scheduling.  

The answer I hypothesize will likely resonate with many is a resounding 'No'. Fortunately, .NET timers provide a reliable and simpler approach. Leveraging the capabilities of .NET timers eliminates the necessity of wrangling with complicated syntax of external libraries or wrestling with the challenges of external schedulers.  

.NET timers provide a simplified internal solution that integrates directly with your codebase, minimizes maintenance overhead, and aligns with the application's lifecycle seamlessly. This way, you maintain control over your scheduled tasks, reducing the likelihood of version control snafus, and simplifying the scheduling process as a whole.

Timers for Effective Scheduling

At its nucleus, a Scheduler behaves akin to a refined timepiece, outlining the following chief behavior: Each task requiring repetitive execution should be associated with an individual clock. These tasks are then executed at the requisite ticks (Uniform/Non-Uniform Interval) specified by their respective clock.  

Given the straightforward nature of this behavior, we could potentially develop a standardized approach centered around this design. The only aspect that might pose as complex at this moment lies in designing a time expression. This expression must be capable of swift parsing and should facilitate the determination of the subsequent occurrence of a scheduled task. This is where the robust CRON expressions come into play, commonly used for their efficiency and ease of use!

Essentially, this marks the entirety of what we require to engineer our personalized Scheduler. Let's now illustrate our solution approach through a detailed diagram for a more comprehensive understanding.

Case Study: An Example Application

Let's swiftly summarize the components of our sample application:

  1. Performer: This is a dedicated service assigned with the actual execution of the job or task.
  1. Scheduler: Functioning as a singleton service, the Scheduler dynamically creates and administratively manages Jobs (Schedules) for the entire application.
  1. Initiator: This service interfaces with the 'Scheduler' to schedule one Job for each requirement within the application. In our demonstration, the HostedService will act as the Initiator.

Let's delve into each component

The Performer:

To lay the groundwork for our Performer, we will establish a simple interface and class tasked with executing a basic job.

// Interface code

public interface ITaskPerformer { Task IWantToBeExecutedEveryMinute(); Task IWantToBeExecutedEveryMonAndFridayAt10AM(); }

// Class code

public class TaskPerformer : ITaskPerformer { public async Task IWantToBeExecutedEveryMinute() { Console.WriteLine($">> I'm executing inside {nameof(IWantToBeExecutedEveryMinute)}"); await Task.Delay(5000); Console.WriteLine($"<< Done executing {nameof(IWantToBeExecutedEveryMinute)}"); } public async Task IWantToBeExecutedEveryMonAndFridayAt10AM() { Console.WriteLine($">> I'm executing inside {nameof(IWantToBeExecutedEveryMonAndFridayAt10AM)}"); await Task.Delay(5000); Console.WriteLine($"<< Done executing {nameof(IWantToBeExecutedEveryMonAndFridayAt10AM)}"); } }

The Scheduler:

The Scheduler is the main protagonist of this blog. In its interface, it reveals methods of Adding and Removing Jobs, facilitating seamless management of scheduled tasks. Hence, a prototypical interface would resemble the following structure:

// Interface code

public interface ITimerBasedScheduler { void AddJob(string uniqueId, string timeExpression, Type serviceType, string invocationMethodName, object invocationObj = null); void RemoveJob(string uniqueId); }

When it comes to the practical implementation of the above interface, the 'AddJob' method is pivotal. A characteristic implementation of this method would entail the subsequent steps:

  1. Initial Delay – Get timespan for the next nearest execution based on the time-expression and create a Timer with the Initial delay.

private TimeSpan GetNextTimeDuration(string timeExpression) { string[] timeExpressions = timeExpression.Split("|"); List times = new List(); DateTime currentDateTime = DateTime.UtcNow; TimeSpan currentTime = TimeSpan.FromTicks(currentDateTime.Ticks); foreach (var expression in timeExpressions) { var cron = CronExpression.Parse(expression.Trim()); times.Add(TimeSpan.FromTicks(cron.GetNextOccurrence(currentDateTime)!.Value.Ticks)); } List durations = times.Select(x => x - currentTime).ToList(); return durations.OrderBy(x => x.Ticks).First(); }

Note - `_schedules` in the above implementation is the `Dictionary<string, Timer>`

  1. Set the callback for invoking the Performer Task.
    // Updated AddJob method code

Note - `_serviceProvider` in the above implementation is the DI Container for the application.

  1. From within the callback, after initiating the Task on separate thread, reset the Timer Delay for the next nearest execution.

    //Updated AddJob method code

The Initiator:

A simplest example of HostedService which can serve as an Initiator and would look like the following:

That’s all! Notice we could schedule 2 Jobs with different schedule requirement with just this much amount of code.  

Following are some benefits of the above approach –  

  1. Simplicity and Enhanced Control: The streamlined process offers a greater degree of control, fostering efficiency.
  1. Lightweight .Net Timers: As some of the most lightweight entities in .Net, timers ensure optimal application performance.
  1. Automation: The process of scheduling jobs can be automated within your applications by storing scheduling details in an external storage entity, such as a Database, thereby increasing maintainability.

Despite the numerous benefits, there exists a minor constraint when using .Net timers for scheduling. The maximum delay/interval of .Net Timers cannot exceed 49 days (UInt32.MaxValue - 1). However, this limitation can be easily circumvented by creating an intermediary mechanism to bypass Performer Task execution. For a comprehensive understanding and implementation details, please refer to the attached sample.

I hope this blog challenged and expanded your understanding of scheduling tools in .Net. Don't forget to share your insights, thoughts, and experience in the comment section below!

Weekly newsletter
No spam. Just the latest releases and tips, interesting articles, and exclusive interviews in your inbox every week.
Read about our privacy policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Contact us

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Take a step closer to the new way!

Explore creative and new approaches with experts by your side.