In this blog post, I am going to walk through how to run background tasks in ASP.Net Core Web applications using infrastructure and API provided by the ASP.Net Core framework.
There are a lot of scenarios where you want to run a task continuously in the background. Often times, we create our own interface and classes and wire them up somehow in the Startup class to achieve this functionality.
In this blog post, I am going to cover two main ways of running background tasks in ASP.Net Core web applications. Both of the ways are provided out of the box by the ASP.Net core framework.
We do not need any external NuGet package for implementing these background tasks.
Application for background tasks
To start this example, first of all I will create a new ASP.NET Core Web API Application.
Firstly, to do that, I will open up Visual Studio 2019. Once Visual Studio opens up, I will select the menu File -> New -> Project. This will open the Create a new Project project popup window.
Secondly, in the Create a new Project popup window, I will select ASP.NET Core Web Application from the project template and click on the Next button.
Thirdly, on the next page, I will provide the name of the application as BackgroundTask.Demo
and click on the Create button.
Finally, on the final page, I will select the API template option. And I will keep other values default (ASP.Net Core 3.1) and click on the Create button.
Two ways of running Background tasks
As I mentioned earlier, we can run background tasks in ASP.NET Core in using two different constructs provided by the ASP.NET Core framework.
They are as follows:
- Firstly, we can implement
IHostedService
interface - Secondly, we can derive from
BackgroundService
abstract base class
Background tasks using IHostedService
Now that the project is ready, it is time to create our first background tasks.
For creating a continuously running background task, let us consider we have a printing process. It will print an incrementing integer number.
To achieve this I will create a new class BackgroundPrinter
. This class will implement the IHostedService
interface.
The IHostedService
interface provides two methods, StartAsync
and StopAsync
. The StartAsync
method is where the task should be started. Whereas the StopAsync
method is where we should implement logic while the task is stopped.
A couple of important point to remember about the StartAsync
method:
- Firstly, the
StartAsync
method is called by the framework before theConfigure
method of theStartup
class is called - Secondly, the
StartAsync
method is called before the server is started
Finally, if the class implementing IHostedService
uses any unmanaged object, the class has to implement IDisposable
interface for disposing off the unmanaged objects.
IHostedService implementation with Timer object
For the first version of the code, I will implement a Timer
inside the BackgroundPrinter
class. And on a timer interval, I will print out the incremented number.
Firstly, I will declare a Timer
object and an integer variable number
in the class.
Secondly, inside the StartAsync
I will create a new instance of the Timer
.
Thirdly, for the Timer
delegate, I will define an anonymous function, where I will increment and print the integer number using ILogger
defined as a class-level variable.
Fourthly, I will set the timer to run in a 5-second interval.
Finally, I will implement the IDisposable
interface and implement the Dispose
method to call the Timer
object’s Dispose
method.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
public class BackgroundPrinter : IHostedService, IDisposable
{
private readonly ILogger<BackgroundPrinter> logger;
private Timer timer;
private int number;
public BackgroundPrinter(ILogger<BackgroundPrinter> logger,
IWorker worker)
{
this.logger = logger;
}
public void Dispose()
{
timer?.Dispose();
}
public Task StartAsync(CancellationToken cancellationToken)
{
timer = new Timer(o => {
Interlocked.Increment(ref number);
logger.LogInformation($"Printing the worker number {number}");
},
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
Configuring the BackgroundPrinter class
Once the BackgroundPrinter
class is ready, now we will configure it to the Dependency Injection container using the AddHostedService
extension method on the IServiceCollection
interface.
Now, we can either do it on the Startup
class or on the Program
class. I prefer doing it in the program class, just for clear separation.
Hence I am going to update the Program
class to achieve this. In the CreateHostBuilder
method, I will use the ConfigureServices
extension method on the IHostBuilder
interface to do this.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}).ConfigureServices(services =>
services.AddHostedService<BackgroundPrinter>());
}
}
Running the application
Now I am going to run the application to see the output in the console.
Once I run the application, I will be able to access the API endpoints. At the same time, the console will print the auto-incremented number every 5 seconds in the background.
Dependency Injection with Background task
In a production scale application, we will probably not be using logic inside of the class that implements IHostedService
. We will not do it so that we can have a separation of concerns and having responsibilities properly defines.
In most cases, the logic of what happens on the task will probably be encapsulated into its own class. Hence, it is very important that the dependency injection works with background tasks.
Now the good news is that since the background tasks are configured into the IServiceCollection
, any types added to the dependency injection container will be available to the background task.
Worker Class
To demonstrate that I will create a class Worker
and interface IWorker
, which will have the logic of incrementing and printing the number. And the BackgroundPrinter
class will just use the interface IWorker
to execute the logic.
The IWorker
interface will have a single method DoWork
. The DoWork
will take a single parameter, an instance of CancellationToken
class.
For the worker class, inside the DoWork
method, instead of running a timer, I will create a while
loop. And the while
loop will wait on the cancel request of the CancellationToken
instance. The CancellationToken
instance will be passed from the StartAsync
method of BackgroundPrinter
class.
Inside the while
loop, I will increment the class level integer and print it out to the console using the ILogger
. And at the end of the while
loop, I will do a Task.Delay
waiting 5 seconds before the loop executes again.
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
public interface IWorker
{
Task DoWork(CancellationToken cancellationToken);
}
}
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
public class Worker : IWorker
{
private readonly ILogger<Worker> logger;
private int number = 0;
public Worker(ILogger<Worker> logger)
{
this.logger = logger;
}
public async Task DoWork(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Interlocked.Increment(ref number);
logger.LogInformation($"Worker printing number {number}");
await Task.Delay(1000 * 5);
}
}
}
}
Once the Worker
class is ready, I will update the BackgroundPrinter
class to use the IWorker
interface for the business logic.
Apart from injection ILogger
, now I will inject IWorker
as well in the constructor of the BackgroundPrinter
class.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
public class BackgroundPrinter : IHostedService
{
private readonly ILogger<BackgroundPrinter> logger;
private readonly IWorker worker;
public BackgroundPrinter(ILogger<BackgroundPrinter> logger,
IWorker worker)
{
this.logger = logger;
this.worker = worker;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await worker.DoWork(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
Finally, I will register the Worker
class in the dependency injection container inside of the Startup
class. I will update the ConfigureServices
method to add the Worker
class as a singleton instance to the dependency injection container.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IWorker, Worker>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
I will not make any changes to the Program
class, since we are still using BackgroundPrinter
class for the background task. Now if I run the application, I will see the exact same response in the console as before.
Background tasks using BackgroundService
Using BackgroundService
abstract base class is the second way of running background tasks. The implementation of BackgroundService
is relatively simpler compared to IHostedService
. But at the same time, you have lesser control over how to start and stop the task.
To demonstrate how BackgroundService
abstract base class works, I will create a new class DerivedBackgroundPrinter
. This new class will drive from the BackgroundService
class.
And this time, I will just use the IWorker
interface for the business logic. Hence, I will create a constructor dependency on IWorker
interface. Since the IWorker
interface is already configured in the dependency injection container, it will automatically be passed along to this class.
The BackgroundService
abstract base class provides a single abstract method ExecuteAsync
. We will have our implementation to call the DoWork
of the IWorker
interface inside of this ExecuteAsync
method.
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundTask.Demo
{
public class DerivedBackgroundPrinter : BackgroundService
{
private readonly IWorker worker;
public DerivedBackgroundPrinter(IWorker worker)
{
this.worker = worker;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await worker.DoWork(stoppingToken);
}
}
}
Once this class is ready, I will update the Program
class, to use DerivedBackgroundPrinter
class instead of BackgroundPrinter
as a background task runner.
Hence, inside of the CreateHostBuilder
method of the Program
class, I will replace the BackgroundPrinter
with DerivedBackgroundPrinter
for the AddHostedService
generic extension method call.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundTask.Demo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}).ConfigureServices(services =>
services.AddHostedService<DerivedBackgroundPrinter>());
}
}
Now if I run the application, I will see the exact same response as before.
Conclusion
As you can see it is extremely simple to create background tasks using ASP.NET Core provided IHostedService
interface as well as BackgroundService
abstract base class. The best part is that there is no external NuGet package dependency.
The source code for this blog is available in my GitHub repository here.
The entire coding session is also available in my YouTube channel here.