Running background tasks in asp.net core

A small overview of the standard tools for launching background tasks in aspnet applications – what is, how it differs, how to use it. The built-in mechanism for launching such tasks is built around the interface IHostedService and an extension method for IServiceCollection – AddHostedService. But there are several ways to implement background tasks through this mechanism (and a few more non-obvious moments of the behavior of this mechanism).

Why run background tasks

There are 2 global background task scripts:

  • One-shot launch of a background task at application startup, waiting for requests to start processing or before. For example, you can migrate data before processing requests, warm up caches or other parts of the application to solve the problem of processing the first requests. You may also need to wait for the application to start – in order to light the beacon of the service in service discovery, having received information about the listening addresses

  • Periodic regular launch of a background task – this can be a health check, sending service telemetry, cache invalidation

aspnet allows you to separate background services and reuse them in different applications, which will provide a universal mechanism for such operations in different services. All of the examples below use the method to register a new background service. ServiceCollectionHostedServiceExtensions.AddHostedService:

services.AddHostedService<MyHostedService>();

Custom implementation of IHostedService

Interface IHostedService provides 2 methods:

public interface IHostedService
{
    // Вызывается, когда приложение готово запустить фоновую службу
    Task StartAsync(CancellationToken stoppingToken);
    // Вызывается, когда происходит нормальное завершение работы узла приложения.
    Task StopAsync(CancellationToken stoppingToken);
}

What is important to know when implementing an interface? Everything IHostedService run sequentially, and the call StartAsync blocks the rest of the application from starting. Therefore, in StartAsync There should be no long blocking operations, unless you really want to delay application startup until the operation is complete (for example, when migrating a database):

This is precisely the feature and motivation to implement background operations through your own implementation. IHostedService – if you need full control over starting and stopping a background service. If this is not so important, then it is enough to inherit from the class BackgroundService.

A couple more important points to implement:

  • At CancellationToken in StopAsync have 5 seconds to complete correctly

  • StopAsync may not be called at all when the application terminates unexpectedly. Therefore, for example, it is not enough to extinguish the beacon only in this method.

The general implementation of a background periodic task in this case might look something like this:

public class MyHostedService : IHostedService
{
    private readonly ISomeBusinessLogicService someService;
 
    public MyHostedService(ISomeBusinessLogicService someService)
    {
        this.someService = someService;
    }
 
    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Не блокируем поток выполнения: StartAsync должен запустить выполнение фоновой задачи и завершить работу
        DoSomeWorkEveryFiveSecondsAsync(cancellationToken);
        return Task.CompletedTask;
    }
 
    private async Task DoSomeWorkEveryFiveSecondsAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await someService.DoSomeWorkAsync();
            }
            catch (Exception ex)
            {
                // обработка ошибки однократного неуспешного выполнения фоновой задачи
            }
 
            await Task.Delay(5000, stoppingToken);
        }
    }
 
    public Task StopAsync(CancellationToken cancellationToken)
    {
        // Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
        await someService.DoSomeCleanupAsync(cancellationToken);
        return Task.CompletedTask;
    }
}

Inheritance from BackgroundService

Background Service is an abstract class that implements IHostedServicehandles start and stop itself by providing 1 abstract method ExecuteAsync:

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)
    {
        _executingTask = ExecuteAsync(_stoppingCts.Token);
        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executingTask == null)
            return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose() => _stoppingCts.Cancel();
}

StartAsync And StopAsync can still be reloaded. Implementation of background tasks via BackgroundService suitable for all scenarios where you do not need to block the application from starting until the operation is completed.

General implementation of a background periodic task:

public class MyHostedService : BackgroundService
{
    private readonly ISomeBusinessLogicService someService;
 
    public MyHostedService(ISomeBusinessLogicService someService)
    {
        this.someService = someService;
    }
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Выполняем задачу пока не будет запрошена остановка приложения
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await someService.DoSomeWorkAsync();
            }
            catch (Exception ex)
            {
                // обработка ошибки однократного неуспешного выполнения фоновой задачи
            }
 
            await Task.Delay(5000);
        }
 
        // Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
        await someService.DoSomeCleanupAsync(cancellationToken);
    }
}

When and how is IHostedService started

Fun fact, the correct answer is “depends”. From the .net version.

In .NET Core 2.x IHostedService were launched after Kestrel was configured and started, that is, after the application starts listening on ports to receive requests. This means that, for example, we can get an object in the background service IServer And IServerAddressesFeature and be sure that when the background service starts, the list of listening addresses will already be configured. It also means that at the time of launch IHostedService the application may already be responding to client requests, so it cannot be guaranteed that at the time the request is processed, any of IHostedService already launched.

In .NET Core 3.0 with the transition to a new abstraction IHost (in fact, the universal node appeared already in .net core 2.1) the behavior has changed – now Kestrel has started to run as a separate IHostedService last after all the others IHostedService. Actually the background services are started before the method Statup.Configure(). Now you can ensure that all other background services are running when you start listening on ports and processing requests, and you can also not start processing requests until one of the background services has finished starting by using an override StartAsync.

Illustration by Andrew Lock https://twitter.com/andrewlocknet
Illustration by Andrew Lock https://twitter.com/andrewlocknet

In .NET 6, things have changed a bit again. Appeared Minimal Hosting APIin which there is no additional abstraction in the form of Startup.cs, and the application is configured explicitly using a new class web application. It should be noted here that the new API is included by default in the aspnet application template, so it will be used for new projects out of the box. Everything IHostedService in this case run when you call WebApplication.Run(), that is, after you have configured the application and the list of listening addresses. More about this is written in issue on github.

In fact, this means that the behavior and available in IHostedService parameters may vary depending on .net version and hosting method, and cannot rely internally on Kestrel to be already configured and running. So if a background service is running with a Kestrel config, then you need a way to wait for it to start inside IHostedService.

Waiting for Kestrel to start inside IHostedService

Starting from version 3.0, asp.net core has a service that allows you to receive notifications that the application has completed launching and has started processing requests – this is IHostApplicationLifetime.

public interface IHostApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }
    CancellationToken ApplicationStopping { get; }
    CancellationToken ApplicationStopped { get; }
    void StopApplication();
}

CancellationToken provides a convenient mechanism for safely launching callbacks when an event occurs:

lifetime.ApplicationStarted.Register(() => DoSomeAction());

Thanks to this, we can wait for the application to start. But while waiting for the launch, we need to handle the situation when there are problems with the start – then the application will never start, and the method that is waiting for the start will not complete. To fix this, it is enough to wait not only ApplicationStartedbut also handle the event for stoppingTokencoming to ExecuteAsync. Here’s what a complete example of a background service would look like that waits for the application to start and correctly handles startup errors:

public class MyHostedService : BackgroundService
{
    private readonly ISomeBusinessLogicService someService;
    private readonly IHostApplicationLifetime lifetime;
 
    public MyHostedService(ISomeBusinessLogicService someService, IHostApplicationLifetime lifetime)
    {
        this.lifetime = lifetime;
        this.someService = someService;
    }
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
       if (!await WaitForAppStartup(lifetime, stoppingToken))
            return;
 
        // Приложение запущено и готово к обработке запросов
 
        // Выполняем задачу пока не будет запрошена остановка приложения
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await someService.DoSomeWorkAsync();
            }
            catch (Exception ex)
            {
                // обработка ошибки однократного неуспешного выполнения фоновой задачи
            }
 
            await Task.Delay(5000);
        }
 
        // Если нужно дождаться завершения очистки, но контролировать время, то стоит предусмотреть в контракте использование CancellationToken
        await someService.DoSomeCleanupAsync(cancellationToken);
    }
 
    static async Task<bool> WaitForAppStartup(IHostApplicationLifetime lifetime, CancellationToken stoppingToken)
    {
        // 👇 Создаём TaskCompletionSource для ApplicationStarted
        var startedSource = new TaskCompletionSource();
        using var reg1 = lifetime.ApplicationStarted.Register(() => startedSource.SetResult());
 
        // 👇 Создаём TaskCompletionSource для stoppingToken
        var cancelledSource = new TaskCompletionSource();
        using var reg2 = stoppingToken.Register(() => cancelledSource.SetResult());
 
        // Ожидаем любое из событий запуска или запроса на остановку
        Task completedTask = await Task.WhenAny(startedSource.Task, cancelledSource.Task).ConfigureAwait(false);
 
        // Если завершилась задача ApplicationStarted, возвращаем true, иначе false
        return completedTask == startedSource.Task;
    }
}

Third party libraries for background tasks

Another popular (57 million downloads) solution for dotnet background tasks is Hangfirea library for setting up, launching and storing background tasks with a free version for commercial use, a lot of settings and a separate task admin.

What to read about background tasks in asp.net

Materials compiled from Microsoft docs articles, Andew Lock articles, and Scott Sauber talk at Rome .NET Conference:

Similar Posts

Leave a Reply