Hôte générique .NET

Dans cet article, vous découvrez les différents modèles de configuration et de génération d’un hôte générique .NET disponible dans le package NuGet Microsoft.Extensions.Hosting. L’hôte générique .NET est responsable de la gestion du démarrage et de la durée de vie de l’application. Les modèles de service Worker créent un hôte générique .NET, HostApplicationBuilder. L’hôte générique peut être utilisé avec d’autres types d’applications .NET, comme les applications de console.

Un hôte est un objet qui encapsule les ressources et les fonctionnalités de durée de vie d’une application, par exemple :

  • Injection de dépendances (DI)
  • Journalisation
  • Configuration
  • Arrêt de l’application
  • Implémentations de IHostedService

Lorsqu’un hôte démarre, il appelle IHostedService.StartAsync sur chaque implémentation de IHostedService inscrite dans la collection de services hébergés du conteneur de services. Dans une application de service Worker, toutes les implémentations IHostedService qui contiennent des instances BackgroundService ont leurs méthodes BackgroundService.ExecuteAsync appelées.

La principale raison d’inclure toutes les ressources interdépendantes de l’application dans un objet est la gestion de la durée de vie : contrôler le démarrage de l’application et l’arrêt approprié.

Configurer un hôte

L’hôte est généralement configuré, généré et exécuté par du code dans la classe Program. La méthode Main :

Les modèles de service Worker .NET génèrent le code suivant pour créer un hôte générique :

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

Pour plus d’informations sur les services Worker, consultez Services Worker dans .NET.

Paramètres du générateur d’hôte

La méthode CreateApplicationBuilder :

  • Définit le chemin retourné par GetCurrentDirectory() comme racine de contenu.
  • Charge la configuration de l’hôte à partir de :
    • Variables d’environnement comportant le préfixe DOTNET_.
    • Arguments de ligne de commande
  • Charge la configuration de l’application à partir de :
    • appsettings.json
    • appsettings.{Environment}.json
    • Secret Manager quand l’application s’exécute dans l’environnement Development.
    • Variables d'environnement.
    • Arguments de ligne de commande
  • Ajoute les fournisseurs de journalisation suivants :
    • Console
    • Déboguer
    • EventSource
    • EventLog (uniquement en cas d’exécution sur Windows)
  • Active la validation de l’étendue et la validation de dépendances lorsque l’environnement est Development.

HostApplicationBuilder.Services constitue une instance de Microsoft.Extensions.DependencyInjection.IServiceCollection. Ces services permettent de créer un IServiceProvider utilisé avec l’injection de dépendances pour résoudre les services inscrits.

Services fournis par le framework

Lorsque vous appelez IHostBuilder.Build() ou HostApplicationBuilder.Build(), les services suivants sont automatiquement inscrits :

IHostApplicationLifetime

Injectez le service IHostApplicationLifetime dans n’importe quelle classe pour gérer les tâches post-démarrage et d’arrêt approprié. Trois propriétés de l’interface sont des jetons d’annulation utilisés pour inscrire les méthodes du gestionnaire d’événements de démarrage et d’arrêt d’application. L’interface inclut également une méthode StopApplication().

L’exemple suivant est un IHostedService et une implémentation IHostedLifecycleService qui inscrit les événements IHostApplicationLifetime :

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("4. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("5. OnStopping has been called.");
    }

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStopped()
    {
        _logger.LogInformation("9. OnStopped has been called.");
    }
}

Le modèle de service Worker peut être modifié pour ajouter l’implémentation ExampleHostedService :

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

L’application écrit l’exemple de sortie suivant :

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.OnStarted has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application started. Press Ctrl+C to shut down.
//     info: Microsoft.Hosting.Lifetime[0]
//           Hosting environment: Production
//     info: Microsoft.Hosting.Lifetime[0]
//           Content root path: ..\app-lifetime\bin\Debug\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

La sortie montre l’ordre de tous les divers événements de cycle de vie :

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

Une fois l’application arrêtée, par exemple avec Ctrl+C, les événements suivants sont déclenchés :

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

L’implémentation de IHostLifetime contrôle quand l’hôte démarre et quand il s’arrête. La dernière implémentation inscrite est utilisée. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime est l’implémentation de IHostLifetime par défaut. Pour plus d’informations sur les mécanismes de durée de vie de l’arrêt, consultez Arrêt de l’hôte.

L’interface IHostLifetime expose une méthode IHostLifetime.WaitForStartAsync appelée au début de IHost.StartAsync qui attend sa fin avant de continuer. Cela permet de retarder le démarrage jusqu'à ce que celui-ci soit signalé par un événement externe.

En outre, l’interface IHostLifetime expose une méthode IHostLifetime.StopAsync appelée à partir de IHost.StopAsync pour indiquer que l’hôte s’arrête et qu’il est temps de s’arrêter.

IHostEnvironment

Injectez le service IHostEnvironment dans une classe pour obtenir des informations sur les paramètres suivants :

En outre, le service IHostEnvironment ouvre la possibilité d’évaluer l’environnement à l’aide des méthodes d’extension suivantes :

Configuration de l’hôte

La configuration de l’hôte est utilisée pour configurer les propriétés de l’implémentation IHostEnvironment.

La configuration de l’hôte est disponible dans la propriété IHostApplicationBuilder.Configuration et l’implémentation de l’environnement est disponible dans la propriété IHostApplicationBuilder.Environment. Pour configurer l’hôte, accédez à la propriété Configuration et appelez l’une des méthodes d’extension disponibles.

Examinez l’exemple suivant pour ajouter la configuration d’hôte :

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Le code précédent :

  • Définit le chemin retourné par GetCurrentDirectory() comme racine de contenu.
  • Charge la configuration de l’hôte à partir de :
    • hostsettings.json.
    • Variables d’environnement comportant le préfixe PREFIX_.
    • Arguments de ligne de commande

la configuration d’une application ;

La configuration d’application est créée en appelant ConfigureAppConfiguration sur un IHostApplicationBuilder. La propriété publique IHostApplicationBuilder.Configuration permet aux consommateurs de lire la configuration existante ou de la modifier à l’aide de méthodes d’extension disponibles.

Pour plus d’informations, consultez Configuration dans .NET.

Arrêt de l’hôte

Il existe plusieurs façons d’arrêter un processus hôte. Le plus souvent, un processus hôte peut être arrêté de la manière suivante :

Le code d’hébergement n’est pas responsable de la gestion de ces scénarios. Le propriétaire du processus doit les traiter de la même façon que n’importe quelle autre application. Il existe plusieurs autres façons d’arrêter un processus de service hébergé :

  • Si ConsoleLifetime est utilisé (UseConsoleLifetime), il écoute les signaux suivants et tente d’arrêter l’hôte correctement.
    • SIGINT (ou CTRL+C).
    • SIGQUIT (ou CTRL+PAUSE sur Windows, CTRL+\ sur Unix).
    • SIGTERM (envoyé par d’autres applications, comme docker stop).
  • Lorsque l’application appelle Environment.Exit.

La logique d’hébergement intégrée gère ces scénarios, en particulier la classe ConsoleLifetime. ConsoleLifetime tente de manipuler les signaux d’arrêt SIGINT, SIGQUIT et SIGTERM pour permettre une sortie appropriée de l’application.

Avant .NET 6, il n’existait pas de moyen pour le code .NET de manipuler correctement SIGTERM. Pour contourner cette limitation, ConsoleLifetime devait s’abonner à System.AppDomain.ProcessExit. Quand ProcessExit était déclenché, ConsoleLifetime indiquait à l’hôte d’arrêter et de bloquer le thread ProcessExit, en attendant que l’hôte s’arrête.

Le gestionnaire de sortie du processus permet d’exécuter le code de nettoyage dans l’application, IHost.StopAsync et le code après HostingAbstractionsHostExtensions.Run dans la méthode Main.

Toutefois, cette approche posait d’autres problèmes, car SIGTERM n’était pas le seul moyen de déclencher ProcessExit. SIGTERM est également déclenché lorsque le code de l’application appelle Environment.Exit. Environment.Exit n’est pas un moyen approprié d’arrêter un processus dans le modèle d’application Microsoft.Extensions.Hosting. Il déclenche l’événement ProcessExit, puis quitte le processus. La fin de la méthode Main n’est pas exécutée. Les threads d’arrière-plan et de premier plan sont terminés et les blocs finally ne sont pas exécutés.

Étant donné que ConsoleLifetime bloquait ProcessExit en attendant que l’hôte s’arrête, ce comportement conduisait à des interblocages à partir du moment où Environment.Exit se bloquait également en attendant l’appel à ProcessExit. En outre, étant donné que la gestion SIGTERM tentait d’arrêter correctement le processus, ConsoleLifetime définissait le ExitCode sur 0, ce qui écrasait le code de sortie de l’utilisateur passé à Environment.Exit.

Dans .NET 6, les signaux POSIX sont pris en charge et manipulés. Le ConsoleLifetime gère correctement SIGTERM, et ne s’implique plus lorsque Environment.Exit est invoqué.

Conseil

Pour .NET 6+, ConsoleLifetime n’a plus de logique pour gérer le scénario Environment.Exit. Les applications qui appellent Environment.Exit et doivent effectuer une logique de nettoyage peuvent s’abonner à ProcessExit elles-mêmes. L’hébergement ne tentera plus d’arrêter correctement l’hôte dans ces scénarios.

Si votre application utilise l’hébergement et que vous souhaitez arrêter correctement l’hôte, vous pouvez appeler IHostApplicationLifetime.StopApplication au lieu de Environment.Exit.

Processus d’arrêt d’hébergement

Le diagramme de séquence suivant montre comment les signaux sont manipulés en interne dans le code d’hébergement. La plupart des utilisateurs n’ont pas besoin de comprendre ce processus. Mais pour les développeurs qui ont besoin d’une compréhension approfondie, un bon visuel peut vous aider à démarrer.

Une fois l’hôte démarré, lorsqu’un utilisateur appelle Run ou WaitForShutdown, un gestionnaire est inscrit pour IApplicationLifetime.ApplicationStopping. L’exécution est suspendue dans WaitForShutdown, en attendant que l’événement ApplicationStopping soit déclenché. La méthode Main ne revient pas immédiatement et l’application continue à s’exécuter jusqu’à ce que Run ou WaitForShutdown revienne.

Lorsqu’un signal est envoyé au processus, il lance la séquence suivante :

Diagramme de séquence d’arrêt d’hébergement.

  1. Le contrôle passe de ConsoleLifetime à ApplicationLifetime pour déclencher l’événement ApplicationStopping. Cela signale à WaitForShutdownAsync de débloquer le code d’exécution Main. En attendant, le gestionnaire de signal POSIX revient avec Cancel = true puisque ce signal POSIX a été manipulé.
  2. Le code d’exécution Main recommence à s’exécuter et indique à l’hôte de StopAsync(), qui à son tour arrête tous les services hébergés et déclenche tous les autres événements arrêtés.
  3. Enfin, WaitForShutdown se ferme, ce qui permet à n’importe quel code de nettoyage d’application de s’exécuter et à la méthode Main de se fermer correctement.

Arrêt de l’hôte dans les scénarios de serveur web

Il existe d’autres scénarios courants dans lesquels l’arrêt normal fonctionne dans Kestrel pour les protocoles HTTP/1.1 et HTTP/2, et comment vous pouvez le configurer dans différents environnements avec un équilibreur de charge pour drainer le trafic en douceur. Bien que la configuration du serveur web dépasse le cadre de cet article, vous trouverez plus d’informations sur Options de configuration de la documentation du serveur web ASP.NET Core Kestrel.

Lorsque l’hôte reçoit un signal d’arrêt (par exemple, CTL+C ou StopAsync), il avertit l’application en signalant ApplicationStopping. Vous devriez vous abonner à cet événement si vous avez des opérations de longue durée qui doivent se terminer correctement.

Ensuite, l’hôte appelle IServer.StopAsync avec un délai d’arrêt que vous pouvez configurer (30s par défaut). Kestrel (et Http.Sys) ferment leurs liaisons de port et cessent d’accepter de nouvelles connexions. Ils indiquent également aux connexions actuelles d’arrêter de traiter les nouvelles requêtes. Pour HTTP/2 et HTTP/3, un message préliminaire GOAWAY est envoyé au client. Pour HTTP/1.1, ils arrêtent la boucle de connexion, car les requêtes sont traitées dans l’ordre. IIS se comporte différemment en rejetant les nouvelles requêtes avec un code d’état 503.

Les requêtes actives ont jusqu’à l’expiration du délai d’arrêt pour se terminer. Si elles sont toutes terminées avant l’expiration du délai, le serveur renvoie le contrôle à l’hôte plus tôt. Si le délai expire, les connexions et requêtes en attente sont interrompues de force, ce qui peut entraîner des erreurs dans les journaux et pour les clients.

Considérations relatives à l’équilibreur de charge

Pour garantir une transition fluide des clients vers une nouvelle destination lors de l’utilisation d’un équilibreur de charge, vous pouvez suivre les étapes suivantes :

  • Affichez la nouvelle instance et commencez à équilibrer le trafic vers celle-ci (il se peut que vous ayez déjà plusieurs instances à des fins de mise à l’échelle).
  • Désactivez ou supprimez l’ancienne instance dans la configuration de l’équilibreur de charge afin qu’elle ne reçoive plus de nouveau trafic.
  • Signalez à l’ancienne instance qu’elle doit s’arrêter.
  • Attendez qu’elle se vide ou qu’elle expire.

Voir aussi