Generischer .NET-Host

In diesem Artikel erfahren Sie mehr über die verschiedenen Muster zum Konfigurieren und Erstellen eines generischen .NET-Hosts, der im NuGet-Paket Microsoft.Extensions.Hosting verfügbar ist. Der generische .NET-Host ist verantwortlich für das Starten der App und das Verwalten der Lebensdauer. Die Workerdienstvorlagen erstellen einen generischen .NET-Host HostApplicationBuilder. Der generische Host kann mit anderen Typen von .NET-Anwendungen verwendet werden, z. B. Konsolen-Apps.

Ein Host ist ein Objekt, das die Ressourcen und Lebensdauerfunktionen einer App kapselt, z. B.:

  • Abhängigkeitsinjektion
  • Protokollierung
  • Konfiguration
  • Herunterfahren der App
  • IHostedService-Implementierungen

Beim Starten eines Hosts wird IHostedService.StartAsync für jede Implementierung von IHostedService aufgerufen, die in der Sammlung gehosteter Dienste des Dienstcontainers registriert ist. In einer Workerdienst-App werden bei allen IHostedService-Implementierungen, die BackgroundService-Instanzen enthalten, die BackgroundService.ExecuteAsync-Methoden aufgerufen.

Der wichtigste Grund für das Einschließen aller unabhängigen Ressourcen der App in einem Objekt ist die Verwaltung der Lebensdauer: steuern von Start und ordnungsgemäßem Herunterfahren der App.

Einrichten eines Hosts

Der Host wird in der Regel per Code in der Program-Klasse konfiguriert, erstellt und ausgeführt. Die Main-Methode:

Die .NET-Workerdienstvorlagen generieren den folgenden Code zum Erstellen eines generischen Hosts:

using Example.WorkerService;

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

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

Weitere Informationen zu Workerdiensten finden Sie unter Workerdienste in .NET.

Einstellungen für Hostgenerator

Die CreateApplicationBuilder-Methode:

  • Legt das Inhaltsstammverzeichnis auf den Pfad fest, der von GetCurrentDirectory() zurückgegeben wird.
  • Lädt Hostkonfiguration aus:
    • Umgebungsvariablen mit dem Präfix DOTNET_.
    • Befehlszeilenargumenten
  • Lädt App-Konfiguration aus:
    • appsettings.json
    • appsettings.{Environment}.json
    • Geheimnis-Manager, wenn die App in der Development-Umgebung ausgeführt wird
    • Umgebungsvariablen.
    • Befehlszeilenargumenten
  • Fügt die folgenden Protokollierungsanbieter hinzu:
    • Konsole
    • Debuggen
    • EventSource
    • EventLog (nur bei Ausführung unter Windows)
  • Aktiviert die Bereichsüberprüfung und die Abhängigkeitsüberprüfung, wenn es sich um eine Development-Umgebung handelt.

HostApplicationBuilder.Services ist eine Microsoft.Extensions.DependencyInjection.IServiceCollection-Instanz. Mit diesen Diensten wird ein IServiceProvider mit Abhängigkeitsinjektion zum Auflösen der registrierten Dienste erstellt.

Von Frameworks bereitgestellte Dienste

Wenn Sie entweder IHostBuilder.Build() oder HostApplicationBuilder.Build() aufrufen, werden die folgenden Dienste automatisch registriert:

IHostApplicationLifetime

Fügen Sie den IHostApplicationLifetime-Dienst durch Injektion in eine beliebige Klasse ein, um die Aufgaben nach dem Starten und die Aufgaben für ordnungsgemäßes Herunterfahren zu verarbeiten. Bei drei der Eigenschaften der Schnittstelle handelt es sich um Abbruchtokens, die zum Registrieren von Ereignishandlermethoden zum Starten und Beenden von Apps verwendet werden. Die Benutzeroberfläche umfasst auch eine StopApplication()-Methode.

Das folgende Beispiel zeigt eine IHostedService- und IHostedLifecycleService-Implementierung, die IHostApplicationLifetime-Ereignisse registriert:

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.");
    }
}

Die Workerdienstvorlage kann geändert werden, um die ExampleHostedService-Implementierung hinzuzufügen:

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();

Die Anwendung schreibt dann die folgende Beispielausgabe:

// 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.

Die Ausgabe zeigt die Reihenfolge aller verschiedenen Lebenszyklusereignisse an:

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

Wenn die Anwendung beendet wird, z. B. mit Strg+C, werden die folgenden Ereignisse ausgelöst:

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

IHostLifetime

Die IHostLifetime-Implementierung steuert, wann der Host gestartet und beendet wird. Die zuletzt registrierte Implementierung wird verwendet. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime ist die IHostLifetime-Standardimplementierung. Weitere Informationen zu den Lebensdauermechanismen des Herunterfahrens finden Sie unter Herunterfahren des Hosts.

Die IHostLifetime-Schnittstelle macht eine IHostLifetime.WaitForStartAsync-Methode verfügbar, die am Anfang von IHost.StartAsync aufgerufen wird. Auf den Abschluss wird gewartet, bevor der Vorgang fortsetzt wird. Dies kann verwendet werden, um den Start zu verzögern, bis dieser durch ein externes Ereignis initiiert wird.

Darüber hinaus macht die IHostLifetime-Schnittstelle eine IHostLifetime.StopAsync-Methode verfügbar, die von IHost.StopAsync aufgerufen wird, um anzugeben, dass der Host beendet wird und es an der Zeit ist, herunterzufahren.

IHostEnvironment

Fügt den IHostEnvironment-Dienst in eine Klasse ein, um Informationen über die folgenden Einstellungen abzurufen:

Darüber hinaus macht der IHostEnvironment-Dienst die Möglichkeit verfügbar, die Umgebung mithilfe dieser Erweiterungsmethoden auszuwerten:

Konfiguration des Hosts

Die Hostkonfiguration wird zum Konfigurieren von Eigenschaften der IHostEnvironment-Implementierung verwendet.

Die Hostkonfiguration ist in der IHostApplicationBuilder.Configuration-Eigenschaft verfügbar, und die Umgebungsimplementierung ist in der IHostApplicationBuilder.Environment-Eigenschaft verfügbar. Um den Host zu konfigurieren, greifen Sie auf die Configuration-Eigenschaft zu, und rufen Sie eine der verfügbaren Erweiterungsmethoden auf.

Sehen Sie sich zum Hinzufügen der Hostkonfiguration das folgende Beispiel an:

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();

Der vorangehende Code:

  • Legt das Inhaltsstammverzeichnis auf den Pfad fest, der von GetCurrentDirectory() zurückgegeben wird.
  • Lädt Hostkonfiguration aus:
    • hostsettings.json.
    • Umgebungsvariablen mit dem Präfix PREFIX_.
    • Befehlszeilenargumenten

App-Konfiguration

Die App-Konfiguration wird durch Aufrufen von ConfigureAppConfiguration für IHostApplicationBuilder erstellt. Die öffentlicheIHostApplicationBuilder.Configuration Eigenschaft ermöglicht es Consumern, mithilfe verfügbarer Erweiterungsmethoden aus der vorhandenen Konfiguration zu lesen oder Änderungen an der vorhandenen Konfiguration vorzunehmen.

Weitere Informationen finden Sie unter Konfiguration in .NET.

Herunterfahren des Hosts

Es gibt mehrere Möglichkeiten, einen gehosteten Prozess zu beenden. Meist kann ein gehosteter Prozess auf folgende Weise beendet werden:

Der Hostingcode ist nicht für die Verarbeitung dieser Szenarien verantwortlich. Der oder die Besitzer*in des Prozesses muss sie wie jede andere Anwendung behandeln. Es gibt mehrere weitere Möglichkeiten, einen gehosteten Dienstprozess zu beenden:

  • Wenn ConsoleLifetime verwendet wird (UseConsoleLifetime), lauscht es auf die folgenden Signale und versucht, den Host ordnungsgemäß zu beenden.
    • SIGINT (oder STRG+C).
    • SIGQUIT (oder STRG+PAUSE unter Windows, STRG+\ unter Unix).
    • SIGTERM (wird von anderen Anwendungen gesendet, z. B. docker stop).
  • Wenn die App Environment.Exit aufruft.

Diese Szenarien werden von der integrierten Hostinglogik verarbeitet, insbesondere von der ConsoleLifetime-Klasse. ConsoleLifetime versucht, das „Herunterfahren“ zu verarbeiten, signalisiert SIGINT, SIGQUIT und SIGTERM, um ein ordnungsgemäßes Beenden der Anwendung zu ermöglichen.

Vor .NET 6 gab es keine Möglichkeit für .NET-Code, SIGTERM ordnungsgemäß zu verarbeiten. Um diese Einschränkung zu umgehen, abonnierte ConsoleLifetimeSystem.AppDomain.ProcessExit. Wenn ProcessExit ausgelöst wurde, signalisierte ConsoleLifetime dem Host, anzuhalten und den ProcessExit-Thread zu blockieren, darauf wartend, dass der Host anhält.

Der Handler zum Beenden des Prozesses könnte den Bereinigungscode in der Anwendung ausführen, z. B. IHost.StopAsync und den Code nach HostingAbstractionsHostExtensions.Run in der Main-Methode.

Es gab jedoch einige Probleme bei diesem Ansatz, da SIGTERM nicht die einzige Ursache für das Auslösen von ProcessExit war. SIGTERM wird auch ausgelöst, wenn App-Code Environment.Exit aufruft. Environment.Exit ist keine gute Möglichkeit, einen Prozess im Microsoft.Extensions.Hosting-App-Modell herunterzufahren. Es löst das ProcessExit-Ereignis aus und beendet den Prozess. Das Ende der Main-Methode wird nicht ausgeführt. Hintergrund- und Vordergrundthreads werden beendet, und finally-Blöcke werden nicht ausgeführt.

Da ConsoleLifetime beim Warten auf das Herunterfahren des Hosts ProcessExit blockierte, führte dieses Verhalten zu Deadlocks, da Environment.Exit beim Warten auf den Aufruf von ProcessExit ebenfalls zu einer Blockade führt. Da die SIGTERM-Behandlung außerdem versuchte, den Prozess ordnungsgemäß herunterzufahren, setzte ConsoleLifetime den ExitCode auf 0, was den an Environment.Exit übergebenen Exitcode des Benutzers beeinträchtigte.

In .NET 6 werden POSIX-Signale unterstützt und verarbeitet. ConsoleLifetime behandelt SIGTERM ordnungsgemäß und wird nicht mehr einbezogen, wenn Environment.Exit aufgerufen wird.

Tipp

Für .NET 6+ verfügt ConsoleLifetime nicht mehr über Logik zur Behandlung des Szenarios Environment.Exit. Apps, die Environment.Exit aufrufen und eine Bereinigungslogik ausführen müssen, können selbst ProcessExit abonnieren. Beim Hosting wird in diesen Szenarien nicht mehr versucht, den Host ordnungsgemäß zu beenden.

Wenn Ihre Anwendung Hosting verwendet und Sie den Host ordnungsgemäß beenden möchten, können Sie IHostApplicationLifetime.StopApplication anstelle von Environment.Exit aufrufen.

Prozess zum Herunterfahren des Hostings

Das folgende Sequenzdiagramm zeigt, wie die Signale intern im Hostingcode behandelt werden. Die meisten Benutzer müssen diesen Vorgang nicht verstehen. Aber Entwickler*innen, bei denen ein umfassendes Verständnis vorausgesetzt werden muss, kann diese Vorstellung den Einstieg vereinfachen.

Wenn ein Benutzer nach dem Start des Hosts Run oder WaitForShutdown aufruft, wird ein Handler für IApplicationLifetime.ApplicationStopping registriert. Die Ausführung wird in WaitForShutdown angehalten, bis das ApplicationStopping-Ereignis ausgelöst wird. Die Rückgabe der Main-Methode erfolgt nicht sofort, und die App wird weiter ausgeführt, bis eine Rückgabe von Run oder WaitForShutdown erfolgt.

Wenn ein Signal an den Prozess gesendet wird, wird die folgende Sequenz initiiert:

Sequenzdiagramm zum Herunterfahren des Hostings.

  1. Die Steuerung fließt von ConsoleLifetime zu ApplicationLifetime, um das ApplicationStopping-Ereignis auszulösen. Dies signalisiert WaitForShutdownAsync, den Main-Ausführungscode freizugeben. In der Zwischenzeit gibt der POSIX-Signalhandler Cancel = true zurück, da dieses POSIX-Signal verarbeitet wurde.
  2. Der Main-Ausführungscode beginnt erneut mit der Ausführung und führt ein StopAsync() für den Host aus, wodurch wiederum alle gehosteten Dienste beendet und alle anderen beendeten Ereignisse ausgelöst werden.
  3. Schließlich wird WaitForShutdown beendet, sodass Anwendungsbereinigungscode ausgeführt und die Main-Methode ordnungsgemäß beendet werden kann.

Herunterfahren des Hosts in Webserverszenarien

Es gibt verschiedene andere gängige Szenarien, in denen das ordnungsgemäße Herunterfahren in Kestrel für die Protokolle HTTP/1.1 und HTTP/2 funktioniert. Sie können dies in verschiedenen Umgebungen mit einem Lastenausgleich konfigurieren, um den Datenverkehr reibungslos auszugleichen. Die Webserverkonfiguration überschreitet den Rahmen dieses Artikels, weitere Informationen finden Sie in der Dokumentation zu den Konfigurationsoptionen für ASP.NET Core Kestrel-Webserver.

Wenn der Host ein Signal zum Herunterfahren empfängt (z. B. CTL+C oder StopAsync), benachrichtigt er die Anwendung, indem er ApplicationStopping signalisiert. Sie sollten dieses Ereignis abonnieren, wenn Sie über Vorgänge mit langer Ausführungsdauer verfügen, die ordnungsgemäß abgeschlossen werden müssen.

Als Nächstes ruft der Host IServer.StopAsync mit einem Timeout für das Herunterfahren auf, das Sie konfigurieren können (Standardwert: 30 s). Kestrel (und Http.Sys) schließen ihre Portbindungen und beenden die Annahme neuer Verbindungen. Außerdem weisen sie die aktuellen Verbindungen an, die Verarbeitung neuer Anforderungen zu beenden. Für HTTP/2 und HTTP/3 wird eine vorläufige GOAWAY-Nachricht an den Client gesendet. Für HTTP/1.1 wird die Verbindungsschleife beendet, da Anforderungen nacheinander verarbeitet werden. IIS verhält sich anders, indem es neue Anforderungen mit dem Statuscode 503 ablehnt.

Die aktiven Anforderungen müssen bis zum Abschluss des Timeouts für das Herunterfahren abgeschlossen werden. Wenn alle vor dem Timeout abgeschlossen sind, gibt der Server die Steuerung früher an den Host zurück. Wenn das Timeout abläuft, wird das Unterbrechen der ausstehenden Verbindungen und Anforderungen erzwungen, was zu Fehlern in den Protokollen und auf Clients führen kann.

Überlegungen zu Load Balancer

Führen Sie die folgenden Schritte aus, um einen reibungslosen Übergang von Clients zu einem neuen Ziel zu gewährleisten, wenn Sie einen Lastenausgleich verwenden:

  • Aktivieren Sie die neue Instanz, und beginnen Sie mit dem Ausgleich des Datenverkehrs (möglicherweise verfügen Sie zu Skalierungszwecken bereits über mehrere Instanzen).
  • Deaktivieren oder entfernen Sie die alte Instanz in der Konfiguration für den Lastenausgleich, damit sie nicht mehr neuen Datenverkehr empfängt.
  • Benachrichtigen Sie die alte Instanz über das Herunterfahren.
  • Warten Sie, bis der Ausgleich oder das Timeout erfolgt.

Siehe auch