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.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. 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 :
- Créer un nouveau projet.
- Sélectionnez Service Worker. Cliquez sur Suivant.
- Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
- Dans la boîte de dialogue Informations supplémentaires, choisissez un Framework. 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(CancellationToken) contient la logique pour démarrer la tâche d’arrière-plan. StartAsync
est appelé avant que :
- Le pipeline de traitement des requêtes de l’application soit configuré.
- Le serveur soit démarré et IApplicationLifetime.ApplicationStarted soit déclenché.
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
- StopAsync(CancellationToken) se déclenche quand l’hôte effectue un arrêt normal.
StopAsync
contient la logique pour terminer la tâche d’arrière-plan. Implémentez IDisposable et les finaliseurs (destructeurs) pour supprimer toutes les ressources non managées.
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 :
- ShutdownTimeout quand vous utilisez l’hôte générique. Pour plus d’informations, consultez Hôte générique .NET dans ASP.NET Core.
- Le paramètre de configuration du délai d’expiration de l’hôte quand vous utilisez l’hôte web. Pour plus d’informations, consultez Hôte web ASP.NET Core.
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 unTask
. À des fins de démonstration, un délai de dix secondes est attendu dans laDoWork
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 uneTask
, qui est attendue dansExecuteAsync
. - 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 serviceMonitorLoop
. 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.
- Trois délais de cinq secondes sont exécutés (
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
:
- Créer un nouveau projet.
- Sélectionnez Service Worker. Cliquez sur Suivant.
- Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
- Dans la boîte de dialogue Informations supplémentaires :
- Choisissez un Framework.
- Cochez la case Activer la publication d’AOA natif.
- 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
- Tests unitaires des services en arrière-plan sur GitHub.
- Affichez ou téléchargez l’exemple de code (procédure de téléchargement)
- Implémenter des tâches d’arrière-plan dans des microservices avec IHostedService et la classe BackgroundService
- Exécuter des tâches en arrière-plan avec WebJobs dans Azure App Service
- Timer
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 :
- Créer un nouveau projet.
- Sélectionnez Service Worker. Cliquez sur Suivant.
- Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Suivant.
- Dans la boîte de dialogue Informations supplémentaires, choisissez un Framework. 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(CancellationToken) contient la logique pour démarrer la tâche d’arrière-plan. StartAsync
est appelé avant que :
- Le pipeline de traitement des requêtes de l’application soit configuré.
- Le serveur soit démarré et IApplicationLifetime.ApplicationStarted soit déclenché.
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
- StopAsync(CancellationToken) se déclenche quand l’hôte effectue un arrêt normal.
StopAsync
contient la logique pour terminer la tâche d’arrière-plan. Implémentez IDisposable et les finaliseurs (destructeurs) pour supprimer toutes les ressources non managées.
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 :
- ShutdownTimeout quand vous utilisez l’hôte générique. Pour plus d’informations, consultez Hôte générique .NET dans ASP.NET Core.
- Le paramètre de configuration du délai d’expiration de l’hôte quand vous utilisez l’hôte web. Pour plus d’informations, consultez Hôte web ASP.NET Core.
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 unTask
. À des fins de démonstration, un délai de dix secondes est attendu dans laDoWork
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 uneTask
, qui est attendue dansExecuteAsync
. - 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 serviceMonitorLoop
. 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.
- Trois délais de cinq secondes sont exécutés (
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
- Tests unitaires des services en arrière-plan sur GitHub.
- Affichez ou téléchargez l’exemple de code (procédure de téléchargement)
- Implémenter des tâches d’arrière-plan dans des microservices avec IHostedService et la classe BackgroundService
- Exécuter des tâches en arrière-plan avec WebJobs dans Azure App Service
- Timer
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 :
- Créer un nouveau projet.
- Sélectionnez Service Worker. Cliquez sur Suivant.
- Indiquez un nom de projet dans le champ Nom du projet, ou acceptez le nom de projet par défaut. Cliquez sur Créer.
- 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 pipeline de traitement des requêtes de l’application soit configuré.
- Le serveur soit démarré et IApplicationLifetime.ApplicationStarted soit déclenché.
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
- StopAsync(CancellationToken) se déclenche quand l’hôte effectue un arrêt normal.
StopAsync
contient la logique pour terminer la tâche d’arrière-plan. Implémentez IDisposable et les finaliseurs (destructeurs) pour supprimer toutes les ressources non managées.
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 :
- ShutdownTimeout quand vous utilisez l’hôte générique. Pour plus d’informations, consultez Hôte générique .NET dans ASP.NET Core.
- Le paramètre de configuration du délai d’expiration de l’hôte quand vous utilisez l’hôte web. Pour plus d’informations, consultez Hôte web ASP.NET Core.
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 unTask
. À des fins de démonstration, un délai de dix secondes est attendu dans laDoWork
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 uneTask
, qui est attendue dansExecuteAsync
. - 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 serviceMonitorLoop
. 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.
- Trois délais de cinq secondes sont exécutés (
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();