Tâches d’arrière-plan avec des services hébergés dans ASP.NET Core

Par Jeow Li Huan

Notes

Il ne s’agit pas de la dernière version de cet article. Pour passer à la dernière version, utilisez le sélecteur de version ASP.NET Core en haut de la table des matières.
Sélecteur de version
Si le sélecteur n’est pas visible dans une fenêtre de navigateur étroite, élargissez la fenêtre ou sélectionnez les points de suspension verticaux (⋮) >Table des matières.
Sélecteur de table des matières

Dans ASP.NET Core, les tâches d’arrière-plan peuvent être implémentées en tant que services hébergés. Un service hébergé est une classe avec la logique de tâches en arrière-plan qui implémente l’interface IHostedService. Cet article fournit trois exemples de services hébergés :

  • Tâche d’arrière-plan qui s’exécute sur un minuteur.
  • Service hébergé qui active un service délimité. Le service étendu peut utiliser l’injection de dépendances .
  • Tâches d’arrière-plan en file d’attente qui s’exécutent séquentiellement.

Modèle Service Worker

Le modèle Service Worker ASP.NET Core fournit un point de départ pour l’écriture d’applications de service durables. Une application créée à partir du modèle Worker Service spécifie le Kit de développement logiciel (SDK) Worker dans son fichier projet :

<Project Sdk="Microsoft.NET.Sdk.Worker">

Pour utiliser le modèle en tant que base d’une application de services hébergés :

  1. Créez un projet.
  2. Sélectionnez Service Worker. Sélectionnez Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Sélectionnez Suivant.
  4. Dans la boîte de dialogue Informations supplémentaires , choisissez une infrastructure. Sélectionnez Create (Créer).

Package

Une application basée sur le modèle Service Worker utilise le Kit de développement logiciel (SDK) Microsoft.NET.Sdk.Worker et possède une référence de package explicite au package Microsoft.Extensions.Hosting. Par exemple, consultez le fichier projet de l’exemple d’application (BackgroundTasksSample.csproj).

Pour les applications web qui utilisent le Microsoft.NET.Sdk.Web Kit de développement logiciel (SDK), le package Microsoft.Extensions.Hosting est référencé implicitement à partir de l’infrastructure partagée. Une référence de package explicite dans le fichier projet de l’application n’est pas nécessaire.

Interface IHostedService

L’interface IHostedService définit deux méthodes pour les objets gérés par l’hôte :

StartAsync

StartAsync(CancellationToken) contient la logique pour démarrer la tâche d’arrière-plan. StartAsync est appelé avant :

StartAsync doit être limité aux tâches en cours d’exécution, car les services hébergés sont exécutés de manière séquentielle et aucun autre service n’est démarré tant StartAsync que les exécutions ne sont pas terminées.

StopAsync

Le jeton d’annulation a un délai d’expiration par défaut de cinq secondes pour indiquer que le processus d’arrêt ne doit plus être normal. Quand l’annulation est demandée sur le jeton :

  • Les opérations en arrière-plan restantes effectuées par l’application doivent être abandonnées.
  • Les méthodes appelées dans StopAsync doivent retourner rapidement.

Toutefois, les tâches ne sont pas abandonnées une fois l’annulation demandée. L’appelant attend que toutes les tâches soient terminées.

Si l’application s’arrête inopinément (par exemple en cas d’échec du processus de l’application), StopAsync n’est probablement pas appelée. Par conséquent, les méthodes appelées ou les opérations effectuées dans StopAsync peuvent ne pas se produire.

Pour prolonger le délai d’expiration par défaut de cinq secondes, définissez :

Le service hébergé est activé une seule fois au démarrage de l’application et s’arrête normalement à l’arrêt de l’application. Si une erreur est levée pendant l’exécution des tâches d’arrière-plan, Dispose doit être appelée même si StopAsync n’est pas appelée.

Classe de base BackgroundService

BackgroundService est une classe de base pour l’implémentation d’une longue exécution IHostedService.

ExecuteAsync(CancellationToken) est appelé pour exécuter le service en arrière-plan. L’implémentation retourne un Task qui représente la durée de vie entière du service en arrière-plan. Aucun autre service n’est démarré tant qu’ExecuteAsync n’est pas asynchrone, par exemple en appelant await. Évitez d’effectuer un travail d’initialisation long et bloquant dans ExecuteAsync. L’hôte se bloque dans StopAsync(CancellationToken) en attente de ExecuteAsync la fin.

Le jeton d’annulation est déclenché lorsque IHostedService.StopAsync est appelé. Votre implémentation de ExecuteAsync doit se terminer rapidement lorsque le jeton d’annulation est déclenché afin d’arrêter normalement le service. Dans le cas contraire, le service s’arrête sans autorisation au moment de l’arrêt. Pour plus d’informations, consultez la section interface IHostedService .

Pour plus d’informations, consultez le code source BackgroundService .

Tâche d’arrière-plan avec minuteur

Une tâche d’arrière-plan avec minuteur utilise la classe System.Threading.Timer. Le minuteur déclenche la méthode DoWork de la tâche. Le minuteur est désactivé sur StopAsync et supprimé quand le conteneur du service est supprimé sur Dispose :

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Le Timer n’attend pas la fin des exécutions précédentes de DoWork pour finir, de sorte que l’approche indiquée peut ne pas convenir à tous les scénarios. Interlocked.Increment est utilisé pour incrémenter le compteur d’exécution en tant qu’opération atomique, ce qui garantit que plusieurs threads ne sont pas mis à jour executionCount simultanément.

Le service est inscrit dans IHostBuilder.ConfigureServices (Program.cs) avec la méthode d’extension AddHostedService :

services.AddHostedService<TimedHostedService>();

Utilisation d’un service délimité dans une tâche d’arrière-plan

Pour utiliser des services délimités dans un backgroundService, créez une étendue. Par défaut, aucune étendue n’est créée pour un service hébergé.

Le service des tâches d’arrière-plan délimitées contient la logique de la tâche d’arrière-plan. Dans l’exemple suivant :

  • Le service est asynchrone. La méthode DoWork retourne un Task. À des fins de démonstration, un délai de dix secondes est attendu dans la DoWork méthode.
  • Un ILogger est injecté dans le service.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

Le service hébergé crée une étendue pour résoudre le service de tâche en arrière-plan délimité pour appeler sa DoWork méthode. DoWork retourne un Task, qui est attendu dans ExecuteAsync :

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Les services sont inscrits dans IHostBuilder.ConfigureServices (Program.cs). Le service hébergé est inscrit avec la méthode d’extension AddHostedService :

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tâches d’arrière-plan en file d’attente

Une file d’attente de tâches en arrière-plan est basée sur .NET 4.x QueueBackgroundWorkItem:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

Dans l’exemple QueueHostedService suivant :

  • La BackgroundProcessing méthode retourne un Task, qui est attendu dans ExecuteAsync.
  • Les tâches d’arrière-plan de la file d’attente sont sorties de la file et exécutées dans BackgroundProcessing.
  • Les éléments de travail sont attendus avant que le service s’arrête dans StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un service MonitorLoop gère les tâches de mise en file d’attente pour le service hébergé chaque fois que la clé w est sélectionnée sur un périphérique d’entrée :

  • La IBackgroundTaskQueue est injectée dans le service MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem est appelée pour mettre un élément de travail en file d’attente.
  • L’élément de travail simule une tâche en arrière-plan de longue durée :
    • Trois délais de 5 secondes sont exécutés (Task.Delay).
    • Une try-catch instruction est OperationCanceledException interceptée si la tâche est annulée.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Les services sont inscrits dans IHostBuilder.ConfigureServices (Program.cs). Le service hébergé est inscrit avec la méthode d’extension AddHostedService :

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop est démarré dans Program.cs:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Tâche en arrière-plan synchronisée asynchrone

Le code suivant crée une tâche en arrière-plan synchronisée asynchrone :

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
    }
}

Ressources supplémentaires

Dans ASP.NET Core, les tâches d’arrière-plan peuvent être implémentées en tant que services hébergés. Un service hébergé est une classe avec la logique de tâches en arrière-plan qui implémente l’interface IHostedService. Cet article fournit trois exemples de services hébergés :

  • Tâche d’arrière-plan qui s’exécute sur un minuteur.
  • Service hébergé qui active un service délimité. Le service étendu peut utiliser l’injection de dépendances .
  • Tâches d’arrière-plan en file d’attente qui s’exécutent séquentiellement.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Modèle Service Worker

Le modèle Service Worker ASP.NET Core fournit un point de départ pour l’écriture d’applications de service durables. Une application créée à partir du modèle Worker Service spécifie le Kit de développement logiciel (SDK) Worker dans son fichier projet :

<Project Sdk="Microsoft.NET.Sdk.Worker">

Pour utiliser le modèle en tant que base d’une application de services hébergés :

  1. Créez un projet.
  2. Sélectionnez Service Worker. Sélectionnez Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Sélectionnez Create (Créer).
  4. Dans la boîte de dialogue Créer un service Worker , sélectionnez Créer.

Package

Une application basée sur le modèle Service Worker utilise le Kit de développement logiciel (SDK) Microsoft.NET.Sdk.Worker et possède une référence de package explicite au package Microsoft.Extensions.Hosting. Par exemple, consultez le fichier projet de l’exemple d’application (BackgroundTasksSample.csproj).

Pour les applications web qui utilisent le Microsoft.NET.Sdk.Web Kit de développement logiciel (SDK), le package Microsoft.Extensions.Hosting est référencé implicitement à partir de l’infrastructure partagée. Une référence de package explicite dans le fichier projet de l’application n’est pas nécessaire.

Interface IHostedService

L’interface IHostedService définit deux méthodes pour les objets gérés par l’hôte :

StartAsync

StartAsync contient la logique pour démarrer la tâche d’arrière-plan. StartAsync est appelé avant :

Le comportement par défaut peut être modifié afin que le service hébergé s’exécute StartAsync une fois que le pipeline de l’application a été configuré et ApplicationStarted est appelé. Pour modifier le comportement par défaut, ajoutez le service hébergé (VideosWatcher dans l’exemple suivant) après avoir appelé ConfigureWebHostDefaults:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
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<VideosWatcher>();
            });
}

StopAsync

Le jeton d’annulation a un délai d’expiration par défaut de cinq secondes pour indiquer que le processus d’arrêt ne doit plus être normal. Quand l’annulation est demandée sur le jeton :

  • Les opérations en arrière-plan restantes effectuées par l’application doivent être abandonnées.
  • Les méthodes appelées dans StopAsync doivent retourner rapidement.

Toutefois, les tâches ne sont pas abandonnées une fois l’annulation demandée. L’appelant attend que toutes les tâches soient terminées.

Si l’application s’arrête inopinément (par exemple en cas d’échec du processus de l’application), StopAsync n’est probablement pas appelée. Par conséquent, les méthodes appelées ou les opérations effectuées dans StopAsync peuvent ne pas se produire.

Pour prolonger le délai d’expiration par défaut de cinq secondes, définissez :

Le service hébergé est activé une seule fois au démarrage de l’application et s’arrête normalement à l’arrêt de l’application. Si une erreur est levée pendant l’exécution des tâches d’arrière-plan, Dispose doit être appelée même si StopAsync n’est pas appelée.

Classe de base BackgroundService

BackgroundService est une classe de base pour l’implémentation d’une longue exécution IHostedService.

ExecuteAsync(CancellationToken) est appelé pour exécuter le service en arrière-plan. L’implémentation retourne un Task qui représente la durée de vie entière du service en arrière-plan. Aucun autre service n’est démarré tant qu’ExecuteAsync n’est pas asynchrone, par exemple en appelant await. Évitez d’effectuer un travail d’initialisation long et bloquant dans ExecuteAsync. L’hôte se bloque dans StopAsync(CancellationToken) en attente de ExecuteAsync la fin.

Le jeton d’annulation est déclenché lorsque IHostedService.StopAsync est appelé. Votre implémentation de ExecuteAsync doit se terminer rapidement lorsque le jeton d’annulation est déclenché afin d’arrêter normalement le service. Dans le cas contraire, le service s’arrête sans autorisation au moment de l’arrêt. Pour plus d’informations, consultez la section interface IHostedService .

StartAsync doit être limité aux tâches en cours d’exécution, car les services hébergés sont exécutés de manière séquentielle et aucun autre service n’est démarré tant StartAsync que les exécutions ne sont pas terminées. Les tâches de longue durée doivent être placées dans ExecuteAsync. Pour plus d’informations, consultez la source de BackgroundService.

Tâche d’arrière-plan avec minuteur

Une tâche d’arrière-plan avec minuteur utilise la classe System.Threading.Timer. Le minuteur déclenche la méthode DoWork de la tâche. Le minuteur est désactivé sur StopAsync et supprimé quand le conteneur du service est supprimé sur Dispose :

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Le Timer n’attend pas la fin des exécutions précédentes de DoWork pour finir, de sorte que l’approche indiquée peut ne pas convenir à tous les scénarios. Interlocked.Increment est utilisé pour incrémenter le compteur d’exécution en tant qu’opération atomique, ce qui garantit que plusieurs threads ne sont pas mis à jour executionCount simultanément.

Le service est inscrit dans IHostBuilder.ConfigureServices (Program.cs) avec la méthode d’extension AddHostedService :

services.AddHostedService<TimedHostedService>();

Utilisation d’un service délimité dans une tâche d’arrière-plan

Pour utiliser des services délimités dans un backgroundService, créez une étendue. Par défaut, aucune étendue n’est créée pour un service hébergé.

Le service des tâches d’arrière-plan délimitées contient la logique de la tâche d’arrière-plan. Dans l’exemple suivant :

  • Le service est asynchrone. La méthode DoWork retourne un Task. À des fins de démonstration, un délai de dix secondes est attendu dans la DoWork méthode.
  • Un ILogger est injecté dans le service.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

Le service hébergé crée une étendue pour résoudre le service de tâche en arrière-plan délimité pour appeler sa DoWork méthode. DoWork retourne un Task, qui est attendu dans ExecuteAsync :

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Les services sont inscrits dans IHostBuilder.ConfigureServices (Program.cs). Le service hébergé est inscrit avec la méthode d’extension AddHostedService :

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tâches d’arrière-plan en file d’attente

Une file d’attente de tâches en arrière-plan est basée sur .NET 4.x QueueBackgroundWorkItem:

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

Dans l’exemple QueueHostedService suivant :

  • La BackgroundProcessing méthode retourne un Task, qui est attendu dans ExecuteAsync.
  • Les tâches d’arrière-plan de la file d’attente sont sorties de la file et exécutées dans BackgroundProcessing.
  • Les éléments de travail sont attendus avant que le service s’arrête dans StopAsync.
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un service MonitorLoop gère les tâches de mise en file d’attente pour le service hébergé chaque fois que la clé w est sélectionnée sur un périphérique d’entrée :

  • La IBackgroundTaskQueue est injectée dans le service MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem est appelée pour mettre un élément de travail en file d’attente.
  • L’élément de travail simule une tâche en arrière-plan de longue durée :
    • Trois délais de 5 secondes sont exécutés (Task.Delay).
    • Une try-catch instruction est OperationCanceledException interceptée si la tâche est annulée.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Les services sont inscrits dans IHostBuilder.ConfigureServices (Program.cs). Le service hébergé est inscrit avec la méthode d’extension AddHostedService :

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop est démarré dans Program.Main:

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

Ressources supplémentaires