Aller au contenu
learn

15. Services hébergés et travail en arrière-plan

Un IHostedService participe au cycle de vie de l’hôte. BackgroundService fournit la boucle ExecuteAsync, mais l’hôte reste propriétaire du démarrage, de la cancellation et de l’arrêt.

Suivez d’abord l’injection de dépendances et les options ainsi que les channels. Vous devez déjà comprendre les durées de vie des services, les cancellation tokens et les flux asynchrones.

L’expérience démarre un hôte, attend des gates de terminaison explicites, traite deux éléments, puis demande l’arrêt. Aucun Thread.Sleep : chaque attente est bornée à cinq secondes.

Dans .NET 10, tout BackgroundService.ExecuteAsync s’exécute sur un thread d’arrière-plan. Le travail qui doit finir avant le démarrage des autres services appartient au constructeur, à StartAsync ou à IHostedLifecycleService, pas avant le premier await d’ExecuteAsync.

Le worker consomme un Channel<T> borné. BoundedChannelFullMode.Wait applique la backpressure quand la file est pleine. Une vraie API de soumission devrait attendre WriteAsync ; cette preuve utilise TryWrite, car sa capacité et ses deux écritures sont fixes.

AddHostedService enregistre le worker comme singleton. Une dépendance scoped doit donc être résolue dans un nouvel IServiceScope pour chaque élément. L’injecter dans le constructeur du worker créerait une dépendance captive.

== The host starts one singleton worker and the queue owns the work
startup observed: True
queued results: C major@scope-1 | G major@scope-2
fresh scope per item: True
shutdown cancellation observed: True

La stratégie par défaut de l’hôte .NET 10 arrête l’application quand un BackgroundService lève une exception. La preuve fixe explicitement BackgroundServiceExceptionBehavior.StopHost, libère une gate de faute et observe ApplicationStopping :

== A BackgroundService fault stops its host
fault requested host stop: True

Arrêter l’hôte n’est pas une reprise. En production, il faut encore définir retry, dead-letter ou intervention opérateur pour le travail répétable sans danger.

Fenêtre de terminal
dotnet run --project code/csharp-advanced/Advanced -c Release -- l15

La sortie vérifiée est expected/l15.txt.

  1. Remplacer TryWrite par une API asynchrone et prouver qu’un troisième élément attend quand les deux places sont occupées.
  2. Ajouter un résultat d’échec par élément sans arrêter le worker singleton.
  3. Introduire un second consommateur et préciser quelle garantie d’ordre disparaît.
Solutions
  1. Renvoyer ValueTask, attendre Writer.WriteAsync et retenir les deux premiers éléments avec des gates. Libérer une gate puis vérifier que la troisième écriture termine, sans déduire un blocage d’un délai.
  2. Capturer l’exception autour d’un seul élément, terminer son TaskCompletionSource avec un échec typé et continuer la boucle. Ne pas traiter la cancellation de l’hôte comme un échec métier.
  3. Mettre SingleReader = false et lancer deux workers. Les lectures restent FIFO, mais l’ordre de fin dépend du traitement ; ajouter des numéros de séquence si l’aval doit réordonner.