Was ist der .NET Generic Host? - Das Fundament für DI, Konfiguration und Logging

· · C#, .NET, Generic Host, Worker, Design

Wenn Sie in .NET eine Konsolenanwendung oder einen Worker zu schreiben beginnen, reicht es anfangs, ein wenig Logik in Main unterzubringen. Sobald die Anwendung aber ein wenig wächst, kommen meist die folgenden Punkte dazu.

  • Sie wollen appsettings.json lesen
  • Sie wollen Werte über Umgebungsvariablen überschreiben
  • Sie wollen mit ILogger Logs ausgeben
  • Sie wollen nicht, dass die Erzeugung von Diensten nur aus new besteht
  • Sie wollen im Hintergrund eine Schleife laufen lassen
  • Sie wollen bei Ctrl+C oder beim Stoppen des Dienstes sauber beenden

Hier kommt der Generic Host ins Spiel. Allerdings lässt sich auch dieser Name leicht verwechseln.

  • Worin unterscheiden sich Host.CreateApplicationBuilder und Host.CreateDefaultBuilder?
  • Ist IHost dasselbe wie ein DI-Container?
  • Wie hängt er mit BackgroundService zusammen?
  • Ist er etwas anderes als ASP.NET Cores WebApplicationBuilder?
  • Lohnt sich der Einsatz auch in einer Konsolenanwendung?

Vermischt sich das, wirkt der Generic Host entweder wie „etwas nur für Webanwendungen“ oder umgekehrt wie „etwas, das man in jeden Host packen sollte“. Beide Sichtweisen sind etwas ungenau.

Dieser Artikel geht vor allem vom praktischen Stand ab .NET 6 aus und klärt zuerst diese vier Punkte.

  • Was der Generic Host tatsächlich ist
  • Worum er sich gebündelt kümmert
  • Die Beziehung zwischen Host.CreateApplicationBuilder / Host.CreateDefaultBuilder / WebApplication.CreateBuilder
  • Wo ein sanfter Einstieg liegt

Inhaltsverzeichnis

  1. Zuerst das Fazit (in einem Satz)
    • 1.1. Zunächst die Begriffe festlegen
  2. Die erste Übersichtstabelle
    • 2.1. Was der Generic Host bündelt
    • 2.2. Die Unterschiede zwischen den Buildern
    • 2.3. Warum es mehrere Einstiegspunkte gibt
  3. Das Gesamtbild des Generic Host (Diagramm)
  4. Was der Generic Host bringt
    • 4.1. Startlogik lässt sich an einer Stelle bündeln
    • 4.2. DI / Konfiguration / Logging sind von Anfang an verbunden
    • 4.3. Sauberes Beenden und residenter Betrieb werden beherrschbar
  5. Minimaler Aufbau
    • 5.1. Ein Minimalbeispiel in einer Konsolenanwendung
    • 5.2. appsettings.json
    • 5.3. Einen BackgroundService hinzufügen
  6. Typische Muster
    • 6.1. Kurzlebige Konsolentools
    • 6.2. Worker / Hintergrunddienste
    • 6.3. Auch unter ASP.NET Core vorhanden
  7. Geeignete Fälle
  8. Ungeeignete / übertriebene Fälle
  9. Stolperfallen
  10. Zusammenfassung
  11. Quellen

1. Zuerst das Fazit (in einem Satz)

  • Der Generic Host ist das Fundament, das Start und Lebensdauer einer .NET-Anwendung an einer Stelle bündelt.
  • Darin enthalten sind DI, Konfiguration, Logging, IHostedService / BackgroundService sowie die Behandlung des Anwendungsendes.
  • Bei einer neuen Nicht-Web-Anwendung ist der naheliegende Einstieg zunächst Host.CreateApplicationBuilder(args).
  • Auch ASP.NET Cores WebApplicationBuilder ist keine andere Welt, sondern derselbe Host-Gedanke, erweitert für das Web.
  • Mit anderen Worten: Der Generic Host ist nicht nur die Geschichte eines einzelnen DI-Containers, sondern der Mechanismus, der den Zusammenbau-Punkt der Anwendung und die Lebenszeitverwaltung bündelt.

Kurz gesagt: Sobald eine Anwendung über „Argumente lesen, einmal ausgeben, fertig“ hinauswächst, zahlt sich der Generic Host deutlich aus. Umgekehrt muss man ihn nicht zwingend in jedes kleine Werkzeug mitbringen, das noch nicht so weit gewachsen ist.

1.1. Zunächst die Begriffe festlegen

Dieser Artikel verwendet im weiteren Verlauf viele Vergleiche, deshalb legen wir zuerst die genauen Bezeichnungen fest.

Begriff Genau genommen Der in diesem Artikel verwendete Vergleich
DI (Dependency Injection / Abhängigkeitsinjektion) Eine Bauweise, bei der eine Klasse die Partner, die sie benötigt, nicht selbst per new erzeugt, sondern von außen übergeben bekommt. Der Ort, an dem die zu übergebenden Partner gesammelt registriert werden, ist der DI-Container (IServiceProvider), und der Generic Host besitzt diesen von Anfang an Verkabelung
Builder (HostApplicationBuilder) Ein Objekt zum Zusammenbauen des Host. Er besitzt Eigenschaften wie Services, Configuration und Logging, bei denen registriert wird. Bis Build() aufgerufen wird, läuft die Anwendung nicht Montagetisch
Host (IHost) Der bereits zusammengebaute Anwendungskörper, den man als Ergebnis von Build() erhält. Er trägt DI-Container, Konfiguration, Logging und Hosted Services in sich und kümmert sich, gestartet mit Run() / RunAsync(), bis zum Stopp um alles Fundament
Hosted Service (IHostedService / BackgroundService) Ein Behälter für Verarbeitung, die synchron zum Start und Stopp des Host läuft. Startet der Host, wird StartAsync aufgerufen, bei einem BackgroundService läuft ExecuteAsync residente Aufgabe
Lifetime Die Verwaltung von Start bis Stopp der Anwendung. Auf Signale wie Ctrl+C, SIGTERM oder das Stoppen eines Dienstes hin wird das Beenden vereinheitlicht Lebensdauer

Am leichtesten vermischt werden Builder und Host. Der Builder ist die zusammenbauende Seite, der Host das Ergebnis des Zusammenbaus, und Build() ist die Grenze dazwischen. Hat man das einmal verinnerlicht, lassen sich auch die im weiteren Verlauf verwendeten Bezeichnungen wie „Fundament“, „Kiste“, „Schalter“ oder „Einstieg“ ohne Verwirrung darüber lesen, worauf sie sich jeweils beziehen.

Wer zum ersten Mal mit DI zu tun hat, liegt mit folgendem Bild richtig: Statt selbst eine Kette von new-Aufrufen zu schreiben, registriert man beim Start: „Wird dieser Typ benötigt, übergeben Sie bitte diese Implementierung“, und die empfangende Seite nimmt sie einfach als Konstruktorargument entgegen. Der Ort für diese Registrierung ist builder.Services.

2. Die erste Übersichtstabelle

2.1. Was der Generic Host bündelt

Zunächst lohnt es sich, den Inhalt dieser Kiste zu sortieren.

Element Worum sich der Generic Host kümmert Warum das hilft
DI Baut Dienste aus IServiceCollection zusammen Erleichtert die Reduzierung von new-Ketten
Configuration Bündelt appsettings.json, Umgebungsvariablen, Befehlszeilenargumente und mehr Erleichtert den Umgang mit umgebungsspezifischen Unterschieden
Logging Schafft die Grundlage für die Verwendung von ILogger<T> Ausgabeziele lassen sich später leicht austauschen
Hosted Service Behandelt Start und Stopp von IHostedService / BackgroundService Erleichtert die Trennung residenter Verarbeitung vom Anwendungskörper
Lifetime Behandelt Start und Stopp über IHostApplicationLifetime, IHostEnvironment und mehr Erleichtert die Vereinheitlichung der Beendigungsart bei Ctrl+C, SIGTERM oder Dienststopp

Wichtig ist hier, dass der Generic Host nicht „ein praktischer DI-Wrapper“ ist. In Wirklichkeit trifft man am wenigsten daneben, wenn man ihn als Kiste betrachtet, die den gesamten Einstiegsbereich der Anwendung gebündelt verkabelt.

2.2. Die Unterschiede zwischen den Buildern

Auch das lässt sich am schnellsten anhand einer einzigen Tabelle erfassen.

Einstieg Hauptverwendung Schreibweise Erste Wahl
Host.CreateApplicationBuilder(args) Neue Nicht-Web-Anwendungen wie Konsole / Worker Direktes Schreiben gegen builder.Services / builder.Configuration / builder.Logging Für Neues diese
Host.CreateDefaultBuilder(args) Bestehender Code oder Konfigurationen, die hauptsächlich auf älteren Erweiterungsmethoden beruhen Verkettung von ConfigureServices und Ähnlichem Bei vorhandenen Altbeständen diese
WebApplication.CreateBuilder(args) ASP.NET Core Webanwendungen / APIs Generic Host plus webspezifische Belange Fürs Web diese

CreateApplicationBuilder und CreateDefaultBuilder sind kein Fall, bei dem die eine ein neues Feature und die andere etwas ganz anderes wäre.

Beide besitzen dieselbe Kernfunktionalität und dasselbe Standardverhalten. Der Unterschied liegt hauptsächlich im Schreibstil.

Bei einer neuen Nicht-Web-Anwendung ist heute Host.CreateApplicationBuilder(args) der naheliegende Einstieg. WebApplication.CreateBuilder(args) lässt sich am einfachsten als derselbe Ablauf verstehen, erweitert zu einem Einstieg für das Web.

2.3. Warum es mehrere Einstiegspunkte gibt

Dass es mehrere Einstiegspunkte gibt, liegt daran, dass sich die Web-Seite und die Nicht-Web-Seite getrennt entwickelt haben und erst später zusammengeführt wurden.

  • Ursprünglich besaß ASP.NET Core einen eigenen Web Host (IWebHostBuilder) nur für das Web, während der Generic Host (IHostBuilder) für Nicht-Web-Anwendungen separat bereitstand.
  • Später wurde ASP.NET Core auf den Generic Host umgestellt, sodass sowohl Web- als auch Nicht-Web-Anwendungen auf demselben Host-Gedanken aufsetzen.
  • Zusätzlich kam neben der Schreibweise mit verketteten Callbacks (ConfigureServices und Ähnliches) eine Schreibweise hinzu, bei der direkt in Eigenschaften geschrieben wird (builder.Services und Ähnliches). Host.CreateApplicationBuilder und WebApplication.CreateBuilder gehören zu Letzterer.

In der aktuellen offiziellen Dokumentation wird die Host.CreateApplicationBuilder-Familie (IHostApplicationBuilder) als für neue Projekte gedacht und Standard in den aktuellen Vorlagen eingeordnet, während die Host.CreateDefaultBuilder-Familie (IHostBuilder) als klassische Vorgehensweise, die zur Kompatibilität mit bestehendem Code erhalten bleibt, beschrieben wird. Ausdrücklich festgehalten ist auch, dass beide dieselbe Kernfunktionalität und dasselbe Standardverhalten besitzen.

Wer von .NET Framework oder .NET Core 3.1 kommt, fragt sich vielleicht, „warum es gleich zwei Schreibweisen gibt“ – doch es hilft, das nicht als zwei nebeneinanderstehende, völlig verschiedene Dinge, sondern als im Zuge der Zusammenführung entstandene zusätzliche Einstiege zu verstehen. Gibt es keinen Grund, sich an Altbestände anzupassen, spricht bei neuen Projekten nichts gegen Host.CreateApplicationBuilder.

3. Das Gesamtbild des Generic Host (Diagramm)

Grob skizziert sieht das Gesamtbild so aus.

args / Umgebungsvariablen / appsettings.jsonHost.CreateApplicationBuilder(args)builder.Configurationbuilder.Servicesbuilder.LoggingIHostedService / BackgroundServicebuilder.Build()IHostRun / RunAsyncStart / Stopp / Ctrl+C / SIGTERM

Normalerweise erzeugt man den Builder in Program.cs, fügt builder.Services Dienste hinzu, passt bei Bedarf builder.Configuration und builder.Logging an, ruft schließlich Build() auf, um ein IHost zu erhalten, und lässt es mit Run() / RunAsync() laufen.

Unauffällig, aber bedeutsam ist, wie viel bereits vorhanden ist, sobald Host.CreateApplicationBuilder(args) aufgerufen wird. Standardmäßig sind zum Beispiel folgende Dinge bereits enthalten.

  • Das Content-Root ist das aktuelle Verzeichnis
  • Die Host-Konfiguration stammt aus Umgebungsvariablen mit dem Präfix DOTNET_ sowie aus Befehlszeilenargumenten
  • Die Anwendungskonfiguration stammt aus appsettings.json, appsettings.{Environment}.json, User Secrets in Development, Umgebungsvariablen und Befehlszeilenargumenten
  • Logging geht an Console / Debug / EventSource / EventLog (nur Windows)
  • In der Umgebung Development sind Scope-Validierung und Abhängigkeitsvalidierung aktiviert

Mit anderen Worten: Man verkabelt nicht gedankenlos bei null, sondern von Anfang an liegt ein Fundament vor, das „für den gewöhnlichen Gebrauch weitgehend ausreicht“.

4. Was der Generic Host bringt

4.1. Startlogik lässt sich an einer Stelle bündeln

Der unauffälligste, aber größte Nutzen des Generic Host ist, dass der Einstiegspunkt der Anwendung nicht so leicht auseinanderdriftet.

Wächst eine Anwendung ein wenig, sammelt sich rund um Main üblicherweise Folgendes an.

  • Laden von Konfigurationsdateien
  • Umgebungsabhängiges Austauschen
  • Initialisierung des Loggers
  • Zusammenbau von HttpClient, Repositories und Services
  • Start der Hintergrundverarbeitung
  • Aufräumarbeiten beim Beendigungssignal

Verdrahtet man das alles ohne Host von Hand, bleibt es zunächst leicht, doch der Einstiegspunkt wird nach und nach zäh.

Mit dem Generic Host wird Program.cs klar als „der Ort, an dem Abhängigkeiten gebündelt zusammengebaut werden“ erkennbar. Allein diese Ordnung verändert die Lesbarkeit bei Code-Reviews erheblich.

4.2. DI / Konfiguration / Logging sind von Anfang an verbunden

Mit dem Generic Host stehen DI, Konfiguration und Logging von Anfang an auf demselben Fundament.

Auf Klassenseite lassen sich zum Beispiel ganz selbstverständlich Dinge wie diese entgegennehmen.

  • ILogger<T>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

Hier zahlt sich aus, dass die Art, Konfiguration zu lesen, und die Art, Dienste zu erzeugen, kaum in unterschiedliche Stile auseinanderdriften.

Sind es nur ein oder zwei Einstellungen, genügt es, IConfiguration["Section:Key"] direkt zu lesen. Wächst die Zahl der Einstellungen in der Praxis jedoch, ist es sicherer, jeden Abschnitt über IOptions<T> in eine Klasse zu bündeln. Als Richtwert gilt: Sobald die Zahl der Schlüsselzeichenketten fünf übersteigt. Ab dieser Größenordnung treten Tippfehler als Fehler auf, die erst zur Laufzeit auffallen, und es wird zunehmend schwerer nachzuvollziehen, welcher Schlüssel wo gelesen wird.

Genauso wird auch beim Logging die Übersicht klarer, wenn man ILogger<T> in die benötigten Klassen injiziert, statt ILoggerFactory überall von Hand zu erzeugen.

Praktisch am Generic Host ist, dass er diese Dinge nicht als getrennte Geschichten behandelt, sondern sie gemeinsam als Fundament der gesamten Anwendung handhabt.

4.3. Sauberes Beenden und residenter Betrieb werden beherrschbar

Der Generic Host kümmert sich nicht nur um „wie gestartet wird“, sondern auch um „wie gestoppt wird“.

Startet der Host, wird bei jedem registrierten IHostedService StartAsync aufgerufen. In Worker-Diensten läuft ExecuteAsync der Hosted Services einschließlich BackgroundService.

„Sauberes Beenden“ bedeutet hier nicht, die Verarbeitung abrupt abzuschneiden, sondern in dieser Reihenfolge zu enden:

  • das Stoppsignal weiterleiten
  • Schleifen und Wartezustände verlassen
  • Verbindungen und Ressourcen aufräumen

Bei lange laufenden Anwendungen ist das sehr wichtig. Ereignisse wie Ctrl+C, SIGTERM oder Dienststopp lassen sich so über die gesamte Anwendung hinweg einheitlich behandeln.

Möchte die Anwendung selbst das Beenden anfordern, steht IHostApplicationLifetime.StopApplication() zur Verfügung. Das Signal „die Arbeit ist erledigt, bitte sauber herunterfahren“ lässt sich so im Kontext des Host ausgeben.

5. Minimaler Aufbau

5.1. Ein Minimalbeispiel in einer Konsolenanwendung

Wichtig zunächst: Die Verwendung des Generic Host bedeutet nicht, dass zwingend ein BackgroundService angelegt werden muss.

Auch für ein Konsolentool, das nur einmal läuft, ist der Generic Host völlig ausreichend, wenn Sie DI, Konfiguration und Logging möchten.

Um ihn nachträglich in ein gewöhnliches Konsolenprojekt einzubauen, referenzieren Sie zunächst Microsoft.Extensions.Hosting.

dotnet add package Microsoft.Extensions.Hosting

Ein Minimalbeispiel für Program.cs sieht etwa so aus.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<JobRunner>();

using IHost host = builder.Build();

try
{
    JobRunner runner = host.Services.GetRequiredService<JobRunner>();
    await runner.RunAsync();
    return 0;
}
catch (Exception ex)
{
    ILogger logger = host.Services
        .GetRequiredService<ILoggerFactory>()
        .CreateLogger("Program");

    logger.LogError(ex, "Unhandled exception occurred during job execution.");
    return 1;
}

internal sealed class JobRunner(
    ILogger<JobRunner> logger,
    IConfiguration configuration,
    IHostEnvironment hostEnvironment)
{
    public Task RunAsync()
    {
        string message = configuration["Sample:Message"] ?? "(no message)";

        logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
        logger.LogInformation("Message: {Message}", message);

        return Task.CompletedTask;
    }
}

Führt man dotnet run aus, erscheint in der Konsole Folgendes (der Wert von Message stammt aus der appsettings.json, die wir im nächsten Abschnitt 5.2 anlegen).

info: JobRunner[0]
      Environment: Production
info: JobRunner[0]
      Message: hello from Generic Host

Rechts von info: stehen die Log-Kategorie (hier der Typname, da es sich um ILogger<JobRunner> handelt) und die Ereignis-ID. Der Standard-Konsolenlogger gibt in dieser Form aus: „Zeile 1 die Kategorie, Zeile 2 der Inhalt.“ Dass Environment hier Production ist, liegt daran, dass dies der Standardwert ist, wenn weder die Umgebungsvariable DOTNET_ENVIRONMENT noch ASPNETCORE_ENVIRONMENT gesetzt ist. Wollen Sie während der Entwicklung umschalten, setzen Sie beim Ausführen DOTNET_ENVIRONMENT=Development.

Bleibt die Anwendung nicht lange resident, müssen Sie nicht bis RunAsync() gehen. Build() aufrufen, die benötigten Dienste auflösen, die Arbeit erledigen und beenden – auch so lässt sich der Nutzen des Generic Host ausreichend nutzen.

Das ist überraschend wichtig. Man muss nicht jedes Mal die Worker-Vorlage auch für kurzlebige Jobs mitbringen.

5.2. appsettings.json

Für das obige Beispiel genügt eine Konfigurationsdatei in dieser minimalen Form.

{
  "Sample": {
    "Message": "hello from Generic Host"
  }
}

Es gibt einen klassischen Stolperstein. In einem Konsolenprojekt wird appsettings.json, wenn Sie sie nur hinzufügen, nicht in den Ausgabeordner kopiert. Stellen Sie in den Projekteigenschaften „In Ausgabeverzeichnis kopieren“ auf „Kopieren, falls neuer“, oder schreiben Sie Folgendes in die csproj.

<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Es lohnt sich, auch das Symptom zu kennen, wenn man das vergisst. Der Generic Host liest appsettings.json als optionale Datei, daher wird ohne sie keine Ausnahme ausgelöst. Es lassen sich lediglich keine Werte abrufen. Im obigen Minimalbeispiel würde dann Message: (no message) angezeigt. Wenn „kein Fehler auftritt, die Konfiguration aber nicht wirkt“, prüfen Sie zuerst, ob sich appsettings.json im Ausgabeordner befindet.

In diesem Beispiel wird configuration["Sample:Message"] direkt gelesen. Wenn Sie nur ein oder zwei Werte betrachten, reicht das aus.

Wächst die Zahl der Einstellungen in der Praxis jedoch, lässt sich das Verstreuen von Schlüsselzeichenketten leichter vermeiden, wenn Sie sich in Richtung

  • Aufteilung nach Abschnitt in jeweils eine Klasse
  • Injektion über IOptions<T>
  • Validierung beim Start

bewegen.

Außerdem sind bei den Standardeinstellungen des Generic Host nicht nur appsettings.json, sondern auch appsettings.{Environment}.json, Umgebungsvariablen und Befehlszeilenargumente verbunden, sodass sich „nur während der Entwicklung austauschen“ und „in der Produktion über Umgebungsvariablen überschreiben“ recht natürlich umsetzen lassen.

5.3. Einen BackgroundService hinzufügen

Für lange laufende Verarbeitung ist die Verwendung von BackgroundService ziemlich naheliegend.

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();

using IHost host = builder.Build();
await host.RunAsync();

internal sealed class PollingWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<PollingWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            using IServiceScope scope = scopeFactory.CreateScope();
            PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();

            await job.RunAsync(stoppingToken);
            logger.LogInformation("Polling completed.");
        }
    }
}

internal sealed class PollingJob(ILogger<PollingJob> logger)
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Do work here.");
        return Task.CompletedTask;
    }
}

An diesem Beispiel lohnt es sich, auf zwei Punkte zu achten.

  1. Der eigentliche Inhalt von BackgroundService ist ExecuteAsync
  2. Wollen Sie scoped Abhängigkeiten, erzeugen Sie mit IServiceScopeFactory einen Scope

BackgroundService selbst besitzt keinen Standard-Scope. Wenn Sie zum Beispiel einen scoped Dienst wie DbContext verwenden möchten, ist es sicher, die Job-Seite wie oben innerhalb eines Scope aufzulösen.

Die Wahl des Werkzeugs für die regelmäßige Ausführung selbst ist ein eigenes Thema, aber wenn Sie async-basiert schreiben, ist PeriodicTimer ziemlich sanft. Das knüpft auch an den verwandten Artikel zu Timern an.

6. Typische Muster

6.1. Kurzlebige Konsolentools

Wie bei Batches, Konvertierungstools oder Wartungsbefehlen lässt sich der Generic Host auch bei Anwendungen, die nur einmal arbeiten und dann enden, ganz normal einsetzen.

Geeignet ist er für Situationen wie diese.

  • Sie wollen Konfigurationsdateien lesen
  • Sie wollen Logs ausgeben
  • Sie wollen HttpClient oder ein Repository injizieren
  • Sie wollen einen Exit-Code zurückgeben

Bringt man bei dieser Art von Anwendung sofort BackgroundService und RunAsync() mit, ist das etwas schwergewichtig und nutzt die Lebenszeitverwaltung des Host übertrieben.

Bei einem kurzlebigen Job genügt es, wie im vorherigen Minimalbeispiel JobRunner aufzulösen und auszuführen.

6.2. Worker / Hintergrunddienste

Bei residenten Workern, Polling, Queue-Konsum, Überwachung oder regelmäßig ausgeführter Verarbeitung ist die Kombination aus Generic Host und BackgroundService ziemlich naheliegend.

Besonders erfreulich ist Folgendes.

  • Der Ablauf von Start und Stopp ist auf Host-Seite vereinheitlicht
  • Logging, Konfiguration und DI stehen von Anfang an zur Verfügung
  • Abbruch lässt sich bei Ctrl+C oder einem Stoppsignal leicht weiterleiten
  • Der Kern der residenten Verarbeitung lässt sich leicht von Program.cs trennen

Außerdem lässt er sich gut mit dem Kontext von Windows Service oder Containern verbinden. Wächst die Anwendung zu einer residenten Anwendung heran, ist der Generic Host ein ziemlich natürliches Fundament.

Beim Umwandeln in einen Windows Service ist es sicherer, Dateien nicht anhand des aktuellen Verzeichnisses zu suchen, sondern von IHostEnvironment.ContentRootPath als Ausgangspunkt auszugehen. Denn der „Basispfad der Anwendung“ wird im Kontext des Host festgelegt.

6.3. Auch unter ASP.NET Core vorhanden

In Web-Anwendungen / APIs wird WebApplication.CreateBuilder(args) verwendet, sodass es auf den ersten Blick wie eine andere Welt als der Generic Host wirken mag.

Vom Gefühl her sind sie jedoch ziemlich eng verbunden.

  • builder.Services
  • builder.Configuration
  • builder.Logging

Dass sich diese so ähnlich schreiben lassen, liegt genau daran.

In ASP.NET Core gehört auch der Start des HTTP-Servers zur Lifetime des Host. Das heißt, das Verständnis des Generic Host zahlt sich auch in dem Sinne aus, dass beim Lesen des Program.cs auf der Web-Seite klarer wird, „warum hier DI, Konfiguration und Logging berührt werden“.

7. Geeignete Fälle

Hier einige Situationen, in denen der Generic Host angenehm passt.

  • Konsolenanwendungen, die Konfiguration, Logging und DI nutzen
  • Worker im Stil von Queue-Consumer, Poller, Watchdog, Scheduler
  • Lange laufende Anwendungen, die bei Ctrl+C oder SIGTERM aufräumen wollen
  • Anwendungen, die künftig zu einem Windows Service oder einer residenten Container-Anwendung heranwachsen könnten
  • Anwendungen, die sich an denselben Erweiterungsstil wie ASP.NET Core angleichen sollen

Gemeinsam ist ihnen, „den Einstiegspunkt und die Lebenszeitverwaltung der Anwendung nicht schludrig behandeln zu wollen“.

Allerdings lässt sich damit allein keine klare Grenze ziehen, deshalb hier noch ein Richtwert. Treffen mindestens zwei der folgenden Punkte zu, macht es sich meist später bezahlt, von Anfang an auf den Generic Host zu setzen.

Richtwert Konkrete Grenze
Anzahl der Einstellungen Es gibt mindestens drei Einstellungen, die je nach Umgebung variieren (Verbindungsziel, Schwellenwert, Ausgabeziel und Ähnliches)
Logging Es muss in einer Datei oder im Event Log festgehalten werden. Ausgabe auf die Standardausgabe allein genügt nicht
Ausführungsform Läuft resident. Oder läuft mindestens einmal am Tag in einem festen Intervall
Abhängigkeiten Es gibt mindestens drei Partner, die Sie über den Konstruktor entgegennehmen möchten, oder Partner, die Sie in Tests austauschen möchten
Lebensdauer Bei Ctrl+C oder Dienststopp sind zwischenzeitliche Aufräumarbeiten nötig
Zukunft Es besteht die Möglichkeit, die Anwendung als Windows Service oder in einem Container zu betreiben

8. Ungeeignete / übertriebene Fälle

Umgekehrt gibt es auch Situationen, in denen der Generic Host von Anfang an nicht die Hauptrolle spielen muss.

  • Kleine Tools, die nur einmal Argumente lesen, einmal ausgeben und enden
  • Wegwerf-Testcode, der nur ein paar Dutzend Minuten verwendet wird
  • Bibliotheksprojekte
  • Fälle, in denen nur eine einzige Einstellung gelesen wird und weder DI noch Logging noch Lebenszeitverwaltung nötig sind

Hier kommen Sie mit weniger Lesearbeit und weniger Dateien aus, wenn Sie direkt in Main schreiben, statt einen Host aufzusetzen. Als Richtwert gilt: Trifft keiner der Punkte aus der Tabelle in Kapitel 7 zu, können Sie den Generic Host bedenkenlos weglassen.

Wichtig ist, dass die Stärke des Generic Host nicht bedeutet, dass er für jede ausführbare Datei zwingend nötig ist.

9. Stolperfallen

Zum Schluss eine Zusammenfassung der Punkte, die man beim ersten Einsatz des Generic Host leicht übersieht.

  • Den Generic Host nur als DI-Container betrachten
    • Tatsächlich ist er ein Fundament, das Start, Stopp, Konfiguration, Logging und Hosted Services umfasst.
  • Bei einer neuen Anwendung aus Gewohnheit mit Host.CreateDefaultBuilder beginnen
    • Gibt es keinen Grund, sich an bestehenden Code anzupassen, ist zunächst Host.CreateApplicationBuilder der naheliegendere Weg.
  • Scoped Dienste direkt in BackgroundService hineinnehmen
    • Hosted Services besitzen keinen Standard-Scope. Sicherer ist es, mit IServiceScopeFactory einen Scope zu erzeugen.
  • Bei einem Worker, der nur einmal endet, dem Host das Stoppen nicht mitteilen
    • Erledigen Sie mit der Worker-Vorlage ein „run once“, läuft der Host einfach weiter, wenn Sie nach getaner Arbeit nicht IHostApplicationLifetime.StopApplication() aufrufen.
  • Sauber beenden wollen, aber mit Environment.Exit abschneiden
    • Verwenden Sie einen Host, ist StopApplication() die sinnvollere Wahl, wenn Sie sauber stoppen möchten.
  • Beim Windows Service das aktuelle Verzeichnis voraussetzen
    • Für die Dateisuche ist es stabiler, von IHostEnvironment.ContentRootPath als Ausgangspunkt auszugehen.
  • Eine kurzlebige CLI von Anfang an in einen BackgroundService einwickeln
    • Bei einer einmaligen Aufgabe genügt es, eine gewöhnliche Service-Klasse aufzulösen und auszuführen.
  • Für die regelmäßige Ausführung eines BackgroundService unbedacht einen Callback-Timer verwenden
    • Schreiben Sie in einem async-Ablauf, ist PeriodicTimer meist besser lesbar und weniger fehleranfällig.

Beim Generic Host reicht es meist schon, zu Beginn zu klären, ob es sich um einen kurzlebigen oder einen residenten Job handelt, um deutlich weniger ins Zögern zu geraten.

10. Zusammenfassung

Auf einen Satz gebracht ist der Generic Host das Fundament, das Einstiegspunkt und Lebenszeitverwaltung einer .NET-Anwendung bündelt.

Blicken wir noch einmal auf die wichtigen Punkte zurück.

  1. Der Generic Host umfasst nicht nur DI, sondern auch Konfiguration, Logging, Beendigungslogik und Hosted Services
  2. Bei einer neuen Nicht-Web-Anwendung ist zunächst Host.CreateApplicationBuilder(args) naheliegend
  3. Bei einem kurzlebigen Job genügt es, auf BackgroundService zu verzichten und einfach zu bauen und auszuführen
  4. Bei residenter Verarbeitung zahlen sich BackgroundService und die Lifetime-Verwaltung des Host deutlich aus
  5. Da BackgroundService keinen Standard-Scope besitzt, muss für scoped Dienste explizit ein Scope erzeugt werden
  6. Auch ASP.NET Cores WebApplicationBuilder steht vom Gedanken her auf demselben Fundament

Der Generic Host ist kein Werkzeug für schwergewichtige Zeremonien. Sobald Konfiguration, Logging, Abhängigkeiten, Start und Beendigung auch nur ein wenig zunehmen, ist er das Werkzeug, um sie nicht in den Wänden verschwinden zu lassen, sondern am Eingang zu bündeln.

Umgekehrt müssen Sie ihn bei kleinen Tools, die das noch nicht brauchen, nicht mitbringen. Gelingt diese Unterscheidung, wird der Generic Host nicht zu „etwas, das man irgendwie einbaut“, sondern zu einem praktischen Fundament mit klar erkennbarem Einsatzort.

11. Quellen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

Es geht um den Aufbau von Windows-Anwendungen mit residenter Verarbeitung, Beendigungslogik, Logging und Konfiguration – als Umsetzungsprojekt passt dieses Thema gut zur Windows-Anwendungsentwicklung.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist der Generic Host?
Der Generic Host ist das Fundament, das Start und Lebensdauer einer .NET-Anwendung an einer Stelle bündelt. Darin enthalten sind DI, Konfiguration (Configuration), Logging, IHostedService / BackgroundService sowie die Behandlung des Anwendungsendes. Er ist kein bloßer DI-Container-Wrapper – am wenigsten daneben liegt man, wenn man ihn als Mechanismus versteht, der den Zusammenbau-Punkt der Anwendung und die Lebenszeitverwaltung an einem Ort zusammenführt. Seine Wirkung zeigt sich bei Anwendungen, bei denen Konfiguration, Logging, Abhängigkeiten, Start und Beendigung auch nur ein wenig zunehmen.
Sollte ich Host.CreateApplicationBuilder oder Host.CreateDefaultBuilder verwenden?
Bei einer neuen Nicht-Web-Anwendung ist der naheliegende Einstieg Host.CreateApplicationBuilder(args). Beide besitzen dieselbe Kernfunktionalität und dasselbe Standardverhalten – es ist nicht so, dass die eine ein neues Feature und die andere etwas ganz anderes wäre. Der Unterschied liegt hauptsächlich im Schreibstil: CreateApplicationBuilder schreibt man direkt gegen builder.Services und Ähnliches, CreateDefaultBuilder verkettet Methoden wie ConfigureServices. Wenn Sie sich an bestehenden Code oder eine Konfiguration anpassen müssen, die hauptsächlich auf älteren Erweiterungsmethoden beruht, wählen Sie CreateDefaultBuilder.
Lohnt sich der Generic Host auch in einer Konsolenanwendung?
Wenn Sie DI, Konfiguration und Logging möchten, ist der Generic Host auch für ein Konsolentool sinnvoll, das nur einmal läuft. Sie müssen dabei nicht zwingend einen BackgroundService anlegen: Build() aufrufen, die benötigten Dienste auflösen, die Arbeit erledigen und beenden – auch in dieser Form bekommen Sie den Nutzen des Generic Host. Umgekehrt ist er für ein kleines Tool, das nur einmal Argumente liest und einmal ausgibt, oder für schnellen Wegwerf-Testcode übertrieben, sodass er nicht jedes Mal zwingend mitgebracht werden muss.
Wie verwendet man scoped Dienste in einem BackgroundService?
Da ein BackgroundService keinen Standard-Scope besitzt, ist es nicht sicher, scoped Dienste direkt per Konstruktorinjektion zu übernehmen. Sicher ist es, IServiceScopeFactory zu injizieren, innerhalb von ExecuteAsync explizit einen Scope zu erzeugen und darin den Dienst der eigentlichen Aufgabe aufzulösen. Besonders wenn Sie einen scoped Dienst wie DbContext verwenden möchten, sollten Sie an dieses Muster denken.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog