Advertisement
Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jun 25, 2022

Run and manage periodic background tasks in ASP.NET Core 6 with C#

Автор:
Philipp Bauknecht
Источник:
Просмотров:
2947

Sometimes your web app needs to do work in the background periodically e.g. to sync data. This article provides a walkthrough how to implement such a background task and how to enabled/disable a background task during runtime using a RESTful API and hosted services.

Here’s the intro from the Microsoft Docs, read more here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services

In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with background task logic that implements the IHostedService interface.

Business Logic

So starting from the empy ASP.NET Core 6 template let’s create a simple sample service that represents our business logic that should be invoked by the periodic background tasks. For this demo it’s enough to have a simple method in here to simulate a task and write to the logger.

class SampleService
{
    private readonly ILogger<SampleService> _logger;

    public SampleService(ILogger<SampleService> logger)
    {
        _logger = logger;
    }

    public async Task DoSomethingAsync()
    {
        await Task.Delay(100);
        _logger.LogInformation(
            "Sample Service did something.");
    }
}

Periodic Background Service

Next we need to create the background service that runs a timer for the periodic invocations. This service needs to implement the IHostedService interface in order to registered as a hosted service. To facilitate handling a hosted service we’ll use the BackgroundService base class that handles most of the hosted service house keeping for us and offers the ExecuteAsync method that’s called to run the background service:

class PeriodicHostedService : BackgroundService
{
    private readonly ILogger<PeriodicHostedService> _logger;

    public PeriodicHostedService(
        ILogger<PeriodicHostedService> logger
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
    }
}

In the ExecuteAsync method we can now add a timer with a while loop. I recommend using the PeriodicTimer as it does not block resources (read more here https://www.ilkayilknur.com/a-new-modern-timer-api-in-dotnet-6-periodictimer). The loop shall run while no cancellation of the background service is requested in the CancellationToken and wait for the next tick of the timer:

private readonly TimeSpan _period = TimeSpan.FromSeconds(5);

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using PeriodicTimer timer = new PeriodicTimer(_period);
    while (
        !stoppingToken.IsCancellationRequested &&
        await timer.WaitForNextTickAsync(stoppingToken))
    {            
    }
}

It’s a good practise to wrap any invocations inside this while loop into a try catch so that when one run of the while loop fails, it doesn’t break the entire method meaning the periodic loop continues:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using PeriodicTimer timer = new PeriodicTimer(_period);
    while (
        !stoppingToken.IsCancellationRequested &&
        await timer.WaitForNextTickAsync(stoppingToken))
    {
        try
        {
        }
        catch (Exception ex)
        {
            _logger.LogInformation(
                $"Failed to execute PeriodicHostedService with exception message {ex.Message}. Good luck next round!");
        }
    }
}

Remember we want to control whether the periodic service is running externally using an API later? So we need a control property and use it in the loop:

private int _executionCount = 0;
public bool IsEnabled { get; set; }

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using PeriodicTimer timer = new PeriodicTimer(_period);
    while (
        !stoppingToken.IsCancellationRequested &&
        await timer.WaitForNextTickAsync(stoppingToken))
    {
        try
        {
            if (IsEnabled)
            {
                _executionCount++;
                _logger.LogInformation(
                    $"Executed PeriodicHostedService - Count: {_executionCount}");
            }
            else
            {
                _logger.LogInformation(
                    "Skipped PeriodicHostedService");
            }
        }
        catch (Exception ex)
        {
            _logger.LogInformation(
                $"Failed to execute PeriodicHostedService with exception message {ex.Message}. Good luck next round!");
        }
    }
}

The last step for our background service is to actually invoke the sample service to execute the business logic. Chances are we want to use a scoped service here. Since no scoped is created for a hosted service by default we need to create one using the IServiceScopeFactory and then get the actual service from the scope. So here is our full class implementation using a scoped service:

class PeriodicHostedService : BackgroundService
{
    private readonly TimeSpan _period = TimeSpan.FromSeconds(5);
    private readonly ILogger<PeriodicHostedService> _logger;
    private readonly IServiceScopeFactory _factory;
    private int _executionCount = 0;
    public bool IsEnabled { get; set; }

    public PeriodicHostedService(
        ILogger<PeriodicHostedService> logger,
        IServiceScopeFactory factory)
    {
        _logger = logger;
        _factory = factory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new PeriodicTimer(_period);
        while (
            !stoppingToken.IsCancellationRequested &&
            await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                if (IsEnabled)
                {
                    await using AsyncServiceScope asyncScope = _factory.CreateAsyncScope();
                    SampleService sampleService = asyncScope.ServiceProvider.GetRequiredService<SampleService>();
                    await sampleService.DoSomethingAsync();
                    _executionCount++;
                    _logger.LogInformation(
                        $"Executed PeriodicHostedService - Count: {_executionCount}");
                }
                else
                {
                    _logger.LogInformation(
                        "Skipped PeriodicHostedService");
                }
            }
            catch (Exception ex)
            {
                _logger.LogInformation(
                    $"Failed to execute PeriodicHostedService with exception message {ex.Message}. Good luck next round!");
            }
        }
    }
}

Manage the background service

To represent the current state of the background service let’s introduce a record with a IsEnabled property:

record PeriodicHostedServiceState(bool IsEnabled);

A get route shall return the current state of our background service:

app.MapGet("/background", (
    PeriodicHostedService service) =>
{
    return new PeriodicHostedServiceState(service.IsEnabled);
});

And a patch route shall let us set the desired state of our background service:

app.MapMethods("/background", new[] { "PATCH" }, (
    PeriodicHostedServiceState state,
    PeriodicHostedService service) =>
{
    service.IsEnabled = state.IsEnabled;
});

Note that we inject the background service into each route above? Since it’s not possible to inject a hosted service through dependency injection we need to add this service as a singleton first, then use it for the hosted service registration:

builder.Services.AddScoped<SampleService>();
builder.Services.AddSingleton<PeriodicHostedService>();
builder.Services.AddHostedService(
    provider => provider.GetRequiredService<PeriodicHostedService>());

Demo

All setup, time to play around!

When starting the app the periodic service will try to execute every 5 seconds as intended but will skip the execution of the business logic as the IsEnabled property is set to false by default.

To enable the background service we need to call the newly created patch route:

curl --location --request PATCH 'https://localhost:7075/background' \
--header 'Content-Type: application/json' \
--data-raw '{
    "IsEnabled": true
}'

After this request the service now is enabled and executes the business logic in the SampleService:

Conclusion

Creating a hosted service and using a timer for periodic updates is straight forward. Just keep in mind you need to create a scope first using the IServiceScopeFactory when you want to use a scoped service in a hosted service and to explicitly add the hosted service as a singleton and using this instance when adding the hosted service to use access the hosted service in a controller/route.

NOTE: When hosting this app e.g. in IIS or Azure App Service make sure the app is set to Always on otherwise the hosted service will be shut down after a while.

Please find the full sample solution here: https://github.com/GrillPhil/PeriodicBackgroundTaskSample

Похожее
Apr 24, 2022
Author: Dr Vin
Let’s find out which one is better .Net 6 or .Net 5 and how .Net has changed the game of software development.In the past, I have briefly talked about the Dot Net 6 framework that was released last year in...
Mar 14
Author: Semih Tekin
Clean Architecture is a software design philosophy that promotes the separation of concerns and the creation of a modular and maintainable codebase. It was introduced by Robert C. Martin, also known as Uncle Bob, and has gained popularity in the...
Jan 2, 2023
Author: Jaydeep Patil
In this article, we are going to discuss the working of CQRS and MediatR patterns and step-by-step implementation using .NET Core 6 Web API.Agenda Introduction of CQRS Pattern When to use CQRS ...
Feb 15
Author: Pranaya Rout
Kestrel Web Server in ASP.NET Core ApplicationIn this article, I will discuss the Kestrel Web Server in ASP.NET Core application with examples. Please read our previous article discussing the ASP.NET Core InProcess Hosting Model with examples. At the end of...
Написать сообщение
Почта
Имя
*Сообщение


© 1999–2024 WebDynamics
1980–... Sergey Drozdov
Area of interests: .NET Framework | .NET Core | C# | ASP.NET | Windows Forms | WPF | HTML5 | CSS3 | jQuery | AJAX | Angular | React | MS SQL Server | Transact-SQL | ADO.NET | Entity Framework | IIS | OOP | OOA | OOD | WCF | WPF | MSMQ | MVC | MVP | MVVM | Design Patterns | Enterprise Architecture | Scrum | Kanban
О кодировках и кодовых страницах
Performance review, ачивки и погоня за повышением грейда — что может причинить боль сотруднику IT-компании?
Вопросы с собеседований, которые означают не то, что вы думаете
NULL в SQL: что это такое и почему его знание необходимо каждому разработчику
Как мы столкнулись с версионированием и осознали, что вариант «просто проставить цифры» не работает
Почему айтишники переходят из одной компании в другую
Еще раз про IP-адреса, маски подсетей и вообще
Три ключевых принципа ПО, которые вы должны понимать
Почему вы никогда не должны соглашаться на собеседования с программированием
Модуль, пакет, библиотека, фреймворк: разбираемся в разнице
LinkedIn
Boosty
Donate to support the project
GitHub account
GitHub profile