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

Par Jeow Li Huan

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 service hébergé :

  • Tâche d’arrière-plan qui s’exécute sur un minuteur.
  • Service hébergé qui active un service étendu. 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 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).

Paquet

Une application basée sur le modèle Worker Service utilise le Microsoft.NET.Sdk.Worker Kit de développement logiciel (SDK) 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é à des tâches en cours d’exécution courtes, car les services hébergés sont exécutés de manière séquentielle et aucun autre service n’est démarré jusqu’à StartAsync la fin de l’exécution.

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 durée IHostedService.

ExecuteAsync(CancellationToken) est appelé pour exécuter le service en arrière-plan. L’implémentation retourne un Task qui représente toute la durée de vie 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. Les blocs hôtes dans StopAsync(CancellationToken) attendent la ExecuteAsync fin.

Le jeton d’annulation est déclenché lorsque IHostedService.StopAsync est appelé. Votre implémentation doit ExecuteAsync se terminer rapidement lorsque le jeton d’annulation est déclenché afin d’arrêter correctement le service. Dans le cas contraire, le service s’arrête de manière non appropriée au délai d’expiration de l’arrêt. Pour plus d’informations, consultez la section de l’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();
    }
}

Il Timer n’attend pas que les exécutions précédentes se DoWork terminent. Par conséquent, l’approche indiquée peut ne pas convenir à chaque scénario. 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 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.
  • 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 étendu 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 suivant QueueHostedService :

  • La BackgroundProcessing méthode retourne un Task, qui est attendu dans ExecuteAsync.
  • Les tâches en arrière-plan dans la file d’attente sont mises en file d’attente et exécutées dans BackgroundProcessing.
  • Les éléments de travail sont attendus avant l’arrêt StopAsyncdu service.
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 MonitorLoop service gère les tâches de mise en file d’attente du service hébergé chaque fois que la w clé est sélectionnée sur un appareil d’entrée :

  • L’injection IBackgroundTaskQueue est injectée dans le MonitorLoop service.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem est appelé pour mettre en file d’attente un élément de travail.
  • L’élément de travail simule une tâche en arrière-plan longue durée :
    • Trois retards de 5 secondes sont exécutés (Task.Delay).
    • Une try-catch instruction intercepte 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();

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 service hébergé :

  • Tâche d’arrière-plan qui s’exécute sur un minuteur.
  • Service hébergé qui active un service étendu. 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.

Afficher ou télécharger un exemple de code (comment télécharger)

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.

Paquet

Une application basée sur le modèle Worker Service utilise le Microsoft.NET.Sdk.Worker Kit de développement logiciel (SDK) 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 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 l’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 durée IHostedService.

ExecuteAsync(CancellationToken) est appelé pour exécuter le service en arrière-plan. L’implémentation retourne un Task qui représente toute la durée de vie 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. Les blocs hôtes dans StopAsync(CancellationToken) attendent la ExecuteAsync fin.

Le jeton d’annulation est déclenché lorsque IHostedService.StopAsync est appelé. Votre implémentation doit ExecuteAsync se terminer rapidement lorsque le jeton d’annulation est déclenché afin d’arrêter correctement le service. Dans le cas contraire, le service s’arrête de manière non appropriée au délai d’expiration de l’arrêt. Pour plus d’informations, consultez la section de l’interface IHostedService .

StartAsync doit être limité à des tâches en cours d’exécution courtes, car les services hébergés sont exécutés de manière séquentielle et aucun autre service n’est démarré jusqu’à StartAsync la fin de l’exécution. Les tâches longues 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();
    }
}

Il Timer n’attend pas que les exécutions précédentes se DoWork terminent. Par conséquent, l’approche indiquée peut ne pas convenir à chaque scénario. 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 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.
  • 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 étendu 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 suivant QueueHostedService :

  • La BackgroundProcessing méthode retourne un Task, qui est attendu dans ExecuteAsync.
  • Les tâches en arrière-plan dans la file d’attente sont mises en file d’attente et exécutées dans BackgroundProcessing.
  • Les éléments de travail sont attendus avant l’arrêt StopAsyncdu service.
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 MonitorLoop service gère les tâches de mise en file d’attente du service hébergé chaque fois que la w clé est sélectionnée sur un appareil d’entrée :

  • L’injection IBackgroundTaskQueue est injectée dans le MonitorLoop service.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem est appelé pour mettre en file d’attente un élément de travail.
  • L’élément de travail simule une tâche en arrière-plan longue durée :
    • Trois retards de 5 secondes sont exécutés (Task.Delay).
    • Une try-catch instruction intercepte 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