Quote of the Day

more Quotes

Categories

Buy me a coffee

  • Home>
  • .NET core>

Hosting a background task in an ASP.NET core application running on IIS.

Published May 22, 2019 in .NET core , ASP.NET core - 3 Comments

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. If you host your ASP.NET core on IIS, check out my other post to see how you can have your application and thus your background task auto start and run continuously on IIS.

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;
// The system invokes StartAsync method at application start up.
// This is a good place to initiate the timer to run your job at
// the specified interval.
public Task StartAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
_logger.LogError("Received cancellation request
before starting timer.");
cancellationToken.ThrowIfCancellationRequested();
}
// Invoke the DoWork method every 5 seconds.
_timer = new Timer(callback: async o => await DoWork(o),
state: null, dueTime: TimeSpan.FromSeconds(0),
period: TimeSpan.FromSeconds(JobIntervalInSecs));
return Task.CompletedTask;
}

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();
    }
// 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.

Interlocked class

That’s all there is to it. As mentioned in the beginning, if your job run on IIS, check out my post to see how to avoid the job from stopping because of the default application pool restarts on IIS.

References

Background tasks with hosted services in ASP.NET Core

https://docs.microsoft.com/en-us/dotnet/api/system.threading.timer?view=netcore-2.2

https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked?view=netframework-4.8

How to auto start and keep an ASP.NET core web application running on IIS

3 comments