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

Par Jeow Li Huan

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

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 contient trois exemples de service hébergé :

  • 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 (DI).
  • 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 de service Worker spécifie le 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éer un nouveau projet.
  2. Sélectionnez Service Worker. Cliquez sur Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
  4. Dans la boîte de dialogue Informations supplémentaires, choisissez un Framework. Cliquez sur 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 SDK Microsoft.NET.Sdk.Web, le package Microsoft.Extensions.Hosting est référencé implicitement à partir du framework partagé. 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 qui sont 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 que :

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 que les exécutions StartAsync ne sont pas terminées.

StopAsync

Le jeton d’annulation a un délai d’expiration par défaut de 30 secondes pour indiquer que le processus d’arrêt ne doit plus être approprié. 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.

Cependant, les tâches ne sont pas abandonnées après la demande d’annulation : l’appelant attend que toutes les tâches se terminent.

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 30 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’un IHostedService à exécution longue.

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 rendu 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 la fin de ExecuteAsync.

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 de façon incorrecte au moment de l’arrêt. Pour plus d’informations, consultez la section Interface IHostedService.

Pour plus d’informations, consultez le code 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 = 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 mettent pas à 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 au sein d’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.
  • Une ILogger est injectée 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 des tâches d’arrière-plan délimitées pour appeler sa méthode DoWork. 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 d’arrière-plan est basée sur QueueBackgroundWorkItem de .NET 4.x :

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 méthode BackgroundProcessing retourne une Task, qui est attendue 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 cinq secondes sont exécutés (Task.Delay).
    • Une instruction try-catch capture OperationCanceledException 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 minutée asynchrone

Le code suivant crée une tâche en arrière-plan minuté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);
    }
}

AOT natif

Les modèles de Service Worker prennent en charge .NET natif à l’avance (AOT) avec l’indicateur --aot :

  1. Créer un nouveau projet.
  2. Sélectionnez Service Worker. Cliquez sur Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
  4. Dans la boîte de dialogue Informations supplémentaires :
  5. Choisissez un Framework.
  6. Cochez la case Activer la publication d’AOA natif.
  7. Cliquez sur Créer.

L’option AOT ajoute <PublishAot>true</PublishAot> au fichier projet :


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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>

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 contient trois exemples de service hébergé :

  • 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 (DI).
  • 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 de service Worker spécifie le 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éer un nouveau projet.
  2. Sélectionnez Service Worker. Cliquez sur Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
  4. Dans la boîte de dialogue Informations supplémentaires, choisissez un Framework. Cliquez sur 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 SDK Microsoft.NET.Sdk.Web, le package Microsoft.Extensions.Hosting est référencé implicitement à partir du framework partagé. 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 qui sont 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 que :

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 que les exécutions StartAsync ne sont pas terminées.

StopAsync

Le jeton d’annulation a un délai d’expiration par défaut de 30 secondes pour indiquer que le processus d’arrêt ne doit plus être approprié. 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.

Cependant, les tâches ne sont pas abandonnées après la demande d’annulation : l’appelant attend que toutes les tâches se terminent.

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 30 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’un IHostedService à exécution longue.

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 rendu 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 la fin de ExecuteAsync.

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 de façon incorrecte au moment de l’arrêt. Pour plus d’informations, consultez la section Interface IHostedService.

Pour plus d’informations, consultez le code 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 = 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 mettent pas à 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 au sein d’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.
  • Une ILogger est injectée 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 des tâches d’arrière-plan délimitées pour appeler sa méthode DoWork. 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 d’arrière-plan est basée sur QueueBackgroundWorkItem de .NET 4.x :

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 méthode BackgroundProcessing retourne une Task, qui est attendue 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 cinq secondes sont exécutés (Task.Delay).
    • Une instruction try-catch capture OperationCanceledException 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 minutée asynchrone

Le code suivant crée une tâche en arrière-plan minuté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 contient trois exemples de service hébergé :

  • 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 (DI).
  • 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 de service Worker spécifie le 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éer un nouveau projet.
  2. Sélectionnez Service Worker. Cliquez sur Suivant.
  3. Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur 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 SDK Microsoft.NET.Sdk.Web, le package Microsoft.Extensions.Hosting est référencé implicitement à partir du framework partagé. 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 qui sont 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 que :

Le comportement par défaut peut être modifié afin que le StartAsync du service hébergé s’exécute une fois le pipeline de l’application configuré et ApplicationStarted 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.

Cependant, les tâches ne sont pas abandonnées après la demande d’annulation : l’appelant attend que toutes les tâches se terminent.

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’un IHostedService à exécution longue.

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 rendu 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 la fin de ExecuteAsync.

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 de façon incorrecte 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 que les exécutions StartAsync 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 mettent pas à 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 au sein d’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.
  • Une ILogger est injectée 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 des tâches d’arrière-plan délimitées pour appeler sa méthode DoWork. 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 d’arrière-plan est basée sur QueueBackgroundWorkItem de .NET 4.x :

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 méthode BackgroundProcessing retourne une Task, qui est attendue 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 cinq secondes sont exécutés (Task.Delay).
    • Une instruction try-catch capture OperationCanceledException 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