In this post, I share how I used the Microsoft.Extensions.Hosting.IHostedService
interface and System.Thread.Timer
class available in ASP.NET core to run a background job at a specified interval. It is straightforward for the most part, and Microsoft provides good documentation on the libraries. One thing that was not clear from the documents was handling overlapping invocations that happens when an invocation starts but the previous one has not finished within the specified interval.
Hosting a service in ASP.NET core
Below are the high level steps of what you need to get your background job running inside of an ASP.NET core application:
- Create a class that implements the
IHostedService
interface. In this class, setup the timer to invoke your job at a specified interval. - Configure dependency injection in Startup.
The IHostedService
interface exposes two primary methods, StartAsync
and StopAsync
. The system calls StartAsync
at application startup and StopAsync
at application shutdown. In the StartAsync
method, you initiate and start the timer that invokes your job at a specified interval.
private readonly int JobIntervalInSecs = 5;<br>// The system invokes StartAsync method at application start up. <br>// This is a good place to initiate the timer to run your job at <br>// the specified interval.<br>public Task StartAsync(CancellationToken cancellationToken) <br>{<br> if (cancellationToken.IsCancellationRequested) <br> {<br> _logger.LogError("Received cancellation request <br> before starting timer.");<br> cancellationToken.ThrowIfCancellationRequested();<br> }<br> // Invoke the DoWork method every 5 seconds. <br> _timer = new Timer(callback: async o => await DoWork(o), <br> state: null, dueTime: TimeSpan.FromSeconds(0), <br> period: TimeSpan.FromSeconds(JobIntervalInSecs));<br> return Task.CompletedTask;<br>}
In the above code snippets, DoWork
is a method that accepts an object. This method is where you would have your business logic for your background job. The example uses async lambdas as the DoWork
method is asynchronous. The Timer class accepts a parameter for storing state of the invocations.
Once you have set and started the timer, it will keep invoking the method at the specified interval, irrespective of whether or not the previous invocation has finished, unless you stop the timer or the application shuts down.
At application shut down, the system calls the StopAsync
method. This method is a good place to stop the timer.
public Task StopAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { _logger.LogError("Received cancellation request before stopping timer."); cancellationToken.ThrowIfCancellationRequested(); }<br> // Change the start time to infinite, thereby stop the timer. _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; }
In the Startup
class, you simply configure your hosted service as in the snippets below:
public void ConfigureServices(IServiceCollection services) { .... services.AddHostedService<JobPDFHostedService>(); }
Above, the JobPDFHostedService
is a class that implements the IHostedService
interface.
Handling overlapping invocations
Depending on the context, you may not want to invoke a service unless the previous call has finished. In my case, I need to fetch and act on the data coming from an azure queue storage. I simply want to throttle the processing rate such that I only check and process an item from the queue at 5 seconds interval and only if no other processing is in progress. Below code snippets show I use to ensure only one invocation is happening at a time, using the System.Threading.Interlocked
class.
private struct State { public static int numberOfActiveJobs = 0; public const int maxNumberOfActiveJobs = 1; } private async Task DoWork(object state) { // allow only a certain number of concurrent work. In this case, // only allow one job to run at a time. if (State.numberOfActiveJobs < State.maxNumberOfActiveJobs) { // Update number of running jobs in one atomic operation. try { Interlocked.Increment(ref State.numberOfActiveJobs); await _jobService.ProcessAsync().ConfigureAwait(false); } finally { Interlocked.Decrement(ref State.numberOfActiveJobs); } } else { _logger.LogDebug("Job skipped since max number of active processes reached."); } }
Before performing the work, I increment the number of active jobs. After performing the work, I decrement the number of active job. Should the timer invoke the DoWork
method before the current invocation has finished, the second invocation simply return without doing any work since the value of numberOfActiveJobs
and maxNumberOfActiveJobs
would be equal.
Using Interlocked.Increment and Interlocked.Decrement methods to update the variables avoid a thread from reading stalled data. From the document,
The Increment and Decrement methods increment or decrement a variable and store the resulting value in a single operation.
Javier is Content Specialist and also .NET developer. He writes helpful guides and articles, assist with other marketing and .NET community work