How to Host a Background Task in ASP.NET Core Application

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:

  1. Create a class that implements the IHostedService interface. In this class, setup the timer to invoke your job at a specified interval.
  2. 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.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *