- Home>
- .NET core>
- Hosting a background task in an ASP.NET core application running on IIS.
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.
Below are the high level steps of what you need to get your background job running inside of an ASP.NET core application:
IHostedService
interface. In this class, setup the timer to invoke your job at a specified interval. 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.
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.
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
Enhancing ASP.NET Core/Blazor App Security and Reusability with HttpMessageHandler and Named HttpClient
Web scraping in C# using HtmlAgilityPack
Building multitenant application – Part 2: Storing value into database session context from ASP.NET core web API
Common frameworks, libraries and design patterns I use
Build and deploy a WebJob alongside web app using azure pipelines
Authenticate against azure ad using certificate in a client credentials flow
Notes on The Clean Architecture
Migrating from Microsoft.AspNetCore.Authentication.AzureAD to Microsoft Identity Web authentication library to integrate with Azure AD.