Implement background tasks in microservices with IHostedService and the BackgroundService class
Tip
This content is an excerpt from the eBook, .NET Microservices Architecture for Containerized .NET Applications, available on .NET Docs or as a free downloadable PDF that can be read offline.
Background tasks and scheduled jobs are something you might need to use in any application, whether or not it follows the microservices architecture pattern. The difference when using a microservices architecture is that you can implement the background task in a separate process/container for hosting so you can scale it down/up based on your need.
From a generic point of view, in .NET we called these type of tasks Hosted Services, because they are services/logic that you host within your host/application/microservice. Note that in this case, the hosted service simply means a class with the background task logic.
Since .NET Core 2.0, the framework provides a new interface named IHostedService helping you to easily implement hosted services. The basic idea is that you can register multiple background tasks (hosted services) that run in the background while your web host or host is running, as shown in the image 6-26.
Figure 6-26. Using IHostedService in a WebHost vs. a Host
ASP.NET Core 1.x and 2.x support IWebHost
for background processes in web apps. .NET Core 2.1 and later versions support IHost
for background processes with plain console apps. Note the difference made between WebHost
and Host
.
A WebHost
(base class implementing IWebHost
) in ASP.NET Core 2.0 is the infrastructure artifact you use to provide HTTP server features to your process, such as when you're implementing an MVC web app or Web API service. It provides all the new infrastructure goodness in ASP.NET Core, enabling you to use dependency injection, insert middlewares in the request pipeline, and similar. The WebHost
uses these very same IHostedServices
for background tasks.
A Host
(base class implementing IHost
) was introduced in .NET Core 2.1. Basically, a Host
allows you to have a similar infrastructure than what you have with WebHost
(dependency injection, hosted services, etc.), but in this case, you just want to have a simple and lighter process as the host, with nothing related to MVC, Web API or HTTP server features.
Therefore, you can choose and either create a specialized host-process with IHost
to handle the hosted services and nothing else, such a microservice made just for hosting the IHostedServices
, or you can alternatively extend an existing ASP.NET Core WebHost
, such as an existing ASP.NET Core Web API or MVC app.
Each approach has pros and cons depending on your business and scalability needs. The bottom line is basically that if your background tasks have nothing to do with HTTP (IWebHost
) you should use IHost
.
Registering hosted services in your WebHost or Host
Let's drill down further on the IHostedService
interface since its usage is pretty similar in a WebHost
or in a Host
.
SignalR is one example of an artifact using hosted services, but you can also use it for much simpler things like:
- A background task polling a database looking for changes.
- A scheduled task updating some cache periodically.
- An implementation of QueueBackgroundWorkItem that allows a task to be executed on a background thread.
- Processing messages from a message queue in the background of a web app while sharing common services such as
ILogger
. - A background task started with
Task.Run()
.
You can basically offload any of those actions to a background task that implements IHostedService
.
The way you add one or multiple IHostedServices
into your WebHost
or Host
is by registering them up through the AddHostedService extension method in an ASP.NET Core WebHost
(or in a Host
in .NET Core 2.1 and above). Basically, you have to register the hosted services within application startup in Program.cs.
//Other DI registrations;
// Register Hosted Services
builder.Services.AddHostedService<GracePeriodManagerService>();
builder.Services.AddHostedService<MyHostedServiceB>();
builder.Services.AddHostedService<MyHostedServiceC>();
//...
In that code, the GracePeriodManagerService
hosted service is real code from the Ordering business microservice in eShopOnContainers, while the other two are just two additional samples.
The IHostedService
background task execution is coordinated with the lifetime of the application (host or microservice, for that matter). You register tasks when the application starts and you have the opportunity to do some graceful action or clean-up when the application is shutting down.
Without using IHostedService
, you could always start a background thread to run any task. The difference is precisely at the app's shutdown time when that thread would simply be killed without having the opportunity to run graceful clean-up actions.
The IHostedService interface
When you register an IHostedService
, .NET calls the StartAsync()
and StopAsync()
methods of your IHostedService
type during application start and stop respectively. For more details, see IHostedService interface.
As you can imagine, you can create multiple implementations of IHostedService and register each of them in Program.cs, as shown previously. All those hosted services will be started and stopped along with the application/microservice.
As a developer, you are responsible for handling the stopping action of your services when StopAsync()
method is triggered by the host.
Implementing IHostedService with a custom hosted service class deriving from the BackgroundService base class
You could go ahead and create your custom hosted service class from scratch and implement the IHostedService
, as you need to do when using .NET Core 2.0 and later.
However, since most background tasks will have similar needs in regard to the cancellation tokens management and other typical operations, there is a convenient abstract base class you can derive from, named BackgroundService
(available since .NET Core 2.1).
That class provides the main work needed to set up the background task.
The next code is the abstract BackgroundService base class as implemented in .NET.
// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts =
new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it,
// this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
When deriving from the previous abstract base class, thanks to that inherited implementation, you just need to implement the ExecuteAsync()
method in your own custom hosted service class, as in the following simplified code from eShopOnContainers which is polling a database and publishing integration events into the Event Bus when needed.
public class GracePeriodManagerService : BackgroundService
{
private readonly ILogger<GracePeriodManagerService> _logger;
private readonly OrderingBackgroundSettings _settings;
private readonly IEventBus _eventBus;
public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
IEventBus eventBus,
ILogger<GracePeriodManagerService> logger)
{
// Constructor's parameters validations...
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogDebug($"GracePeriodManagerService is starting.");
stoppingToken.Register(() =>
_logger.LogDebug($" GracePeriod background task is stopping."));
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogDebug($"GracePeriod task doing background work.");
// This eShopOnContainers method is querying a database table
// and publishing events into the Event Bus (RabbitMQ / ServiceBus)
CheckConfirmedGracePeriodOrders();
try {
await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
}
catch (TaskCanceledException exception) {
_logger.LogCritical(exception, "TaskCanceledException Error", exception.Message);
}
}
_logger.LogDebug($"GracePeriod background task is stopping.");
}
.../...
}
In this specific case for eShopOnContainers, it's executing an application method that's querying a database table looking for orders with a specific state and when applying changes, it is publishing integration events through the event bus (underneath it can be using RabbitMQ or Azure Service Bus).
Of course, you could run any other business background task, instead.
By default, the cancellation token is set with a 5 seconds timeout, although you can change that value when building your WebHost
using the UseShutdownTimeout
extension of the IWebHostBuilder
. This means that our service is expected to cancel within 5 seconds otherwise it will be more abruptly killed.
The following code would be changing that time to 10 seconds.
WebHost.CreateDefaultBuilder(args)
.UseShutdownTimeout(TimeSpan.FromSeconds(10))
...
Summary class diagram
The following image shows a visual summary of the classes and interfaces involved when implementing IHostedServices.
Figure 6-27. Class diagram showing the multiple classes and interfaces related to IHostedService
Class diagram: IWebHost and IHost can host many services, which inherit from BackgroundService, which implements IHostedService.
Deployment considerations and takeaways
It is important to note that the way you deploy your ASP.NET Core WebHost
or .NET Host
might impact the final solution. For instance, if you deploy your WebHost
on IIS or a regular Azure App Service, your host can be shut down because of app pool recycles. But if you are deploying your host as a container into an orchestrator like Kubernetes, you can control the assured number of live instances of your host. In addition, you could consider other approaches in the cloud especially made for these scenarios, like Azure Functions. Finally, if you need the service to be running all the time and are deploying on a Windows Server you could use a Windows Service.
But even for a WebHost
deployed into an app pool, there are scenarios like repopulating or flushing application's in-memory cache that would be still applicable.
The IHostedService
interface provides a convenient way to start background tasks in an ASP.NET Core web application (in .NET Core 2.0 and later versions) or in any process/host (starting in .NET Core 2.1 with IHost
). Its main benefit is the opportunity you get with the graceful cancellation to clean-up the code of your background tasks when the host itself is shutting down.
Additional resources
Building a scheduled task in ASP.NET Core/Standard 2.0
https://blog.maartenballiauw.be/post/2017/08/01/building-a-scheduled-cache-updater-in-aspnet-core-2.htmlImplementing IHostedService in ASP.NET Core 2.0
https://www.stevejgordon.co.uk/asp-net-core-2-ihostedserviceGenericHost Sample using ASP.NET Core 2.1
https://github.com/aspnet/Hosting/tree/release/2.1/samples/GenericHostSample