Was ist der .NET Generic Host? - Das Fundament für DI, Konfiguration und Logging
· Go Komura · 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.jsonlesen - Sie wollen Werte über Umgebungsvariablen überschreiben
- Sie wollen mit
ILoggerLogs ausgeben - Sie wollen nicht, dass die Erzeugung von Diensten nur aus
newbesteht - Sie wollen im Hintergrund eine Schleife laufen lassen
- Sie wollen bei
Ctrl+Coder 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.CreateApplicationBuilderundHost.CreateDefaultBuilder? - Ist
IHostdasselbe wie ein DI-Container? - Wie hängt er mit
BackgroundServicezusammen? - 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
- Zuerst das Fazit (in einem Satz)
- 1.1. Zunächst die Begriffe festlegen
- 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
- Das Gesamtbild des Generic Host (Diagramm)
- 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
- Minimaler Aufbau
- 5.1. Ein Minimalbeispiel in einer Konsolenanwendung
- 5.2.
appsettings.json - 5.3. Einen
BackgroundServicehinzufügen
- Typische Muster
- 6.1. Kurzlebige Konsolentools
- 6.2. Worker / Hintergrunddienste
- 6.3. Auch unter ASP.NET Core vorhanden
- Geeignete Fälle
- Ungeeignete / übertriebene Fälle
- Stolperfallen
- Zusammenfassung
- 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/BackgroundServicesowie die Behandlung des Anwendungsendes. - Bei einer neuen Nicht-Web-Anwendung ist der naheliegende Einstieg zunächst
Host.CreateApplicationBuilder(args). - Auch ASP.NET Cores
WebApplicationBuilderist 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 (
ConfigureServicesund Ähnliches) eine Schreibweise hinzu, bei der direkt in Eigenschaften geschrieben wird (builder.Servicesund Ähnliches).Host.CreateApplicationBuilderundWebApplication.CreateBuildergehö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.
flowchart LR
Args["args / Umgebungsvariablen / appsettings.json"] --> Builder["Host.CreateApplicationBuilder(args)"]
Builder --> Config["builder.Configuration"]
Builder --> Services["builder.Services"]
Builder --> Logging["builder.Logging"]
Services --> Hosted["IHostedService / BackgroundService"]
Builder --> Build["builder.Build()"]
Build --> Host["IHost"]
Host --> Run["Run / RunAsync"]
Run --> Lifetime["Start / Stopp / Ctrl+C / SIGTERM"]
Lifetime --> Hosted
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
Developmentsind 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>IConfigurationIHostEnvironmentIOptions<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.
- Der eigentliche Inhalt von
BackgroundServiceistExecuteAsync - Wollen Sie scoped Abhängigkeiten, erzeugen Sie mit
IServiceScopeFactoryeinen 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
HttpClientoder 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+Coder einem Stoppsignal leicht weiterleiten - Der Kern der residenten Verarbeitung lässt sich leicht von
Program.cstrennen
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.Servicesbuilder.Configurationbuilder.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+Coder 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.CreateDefaultBuilderbeginnen- Gibt es keinen Grund, sich an bestehenden Code anzupassen, ist zunächst
Host.CreateApplicationBuilderder naheliegendere Weg.
- Gibt es keinen Grund, sich an bestehenden Code anzupassen, ist zunächst
- Scoped Dienste direkt in
BackgroundServicehineinnehmen- Hosted Services besitzen keinen Standard-Scope. Sicherer ist es, mit
IServiceScopeFactoryeinen Scope zu erzeugen.
- Hosted Services besitzen keinen Standard-Scope. Sicherer ist es, mit
- 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.
- Erledigen Sie mit der Worker-Vorlage ein „run once“, läuft der Host einfach weiter, wenn Sie nach getaner Arbeit nicht
- Sauber beenden wollen, aber mit
Environment.Exitabschneiden- Verwenden Sie einen Host, ist
StopApplication()die sinnvollere Wahl, wenn Sie sauber stoppen möchten.
- Verwenden Sie einen Host, ist
- Beim Windows Service das aktuelle Verzeichnis voraussetzen
- Für die Dateisuche ist es stabiler, von
IHostEnvironment.ContentRootPathals Ausgangspunkt auszugehen.
- Für die Dateisuche ist es stabiler, von
- Eine kurzlebige CLI von Anfang an in einen
BackgroundServiceeinwickeln- 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
BackgroundServiceunbedacht einen Callback-Timer verwenden- Schreiben Sie in einem
async-Ablauf, istPeriodicTimermeist besser lesbar und weniger fehleranfällig.
- Schreiben Sie in einem
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.
- Der Generic Host umfasst nicht nur DI, sondern auch Konfiguration, Logging, Beendigungslogik und Hosted Services
- Bei einer neuen Nicht-Web-Anwendung ist zunächst
Host.CreateApplicationBuilder(args)naheliegend - Bei einem kurzlebigen Job genügt es, auf
BackgroundServicezu verzichten und einfach zu bauen und auszuführen - Bei residenter Verarbeitung zahlen sich
BackgroundServiceund die Lifetime-Verwaltung des Host deutlich aus - Da
BackgroundServicekeinen Standard-Scope besitzt, muss für scoped Dienste explizit ein Scope erzeugt werden - Auch ASP.NET Cores
WebApplicationBuildersteht 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
- .NET Generic Host - .NET
- Worker Services in .NET
- Use scoped services within a BackgroundService - .NET
- Configuration in .NET
- Options pattern in .NET
- .NET Generic Host in ASP.NET Core
- Create Windows Service using BackgroundService - .NET
- Verwandter Artikel: Die drei .NET-Timer im Vergleich - PeriodicTimer/Timer/DispatcherTimer
- Verwandter Artikel: C# async/await in der Praxis - Entscheidungstabelle für Task.Run und ConfigureAwait
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Warum .NET Generic Host und BackgroundService in Desktop-Apps verwenden
Wie sich Generic Host und BackgroundService nutzen lassen, um Start, periodische Verarbeitung, Beendigung, Logging, Konfiguration und DI ...
Praktische Best Practices für Multithreading: .NET-Edition — Was Sie entscheiden sollten, bevor Sie weitere Threads hinzufügen
Eine praxisnahe Übersicht der Entwurfsregeln, die verhindern, dass Multithreading-Code in .NET/C# gelegentlich abstürzt oder hängen bleib...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Windows-Dienste erstellen und betreiben ── Von der Abgrenzung zur Aufgabenplanung bis zur Umwandlung eines BackgroundService in einen Dienst
Sollte ein Hintergrundprozess ein Windows-Dienst werden, oder reicht die Aufgabenplanung? Dieser Artikel ordnet die Entscheidungstabelle,...
Was ist .NET Native AOT? - Der Unterschied zu JIT und Trimming
Wir ordnen ein, was Native AOT ist, anhand der Unterschiede zu JIT, ReadyToRun, self-contained, single-file, trimming und Source Generato...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Generic Host und Anwendungsarchitektur
Generic Host, BackgroundService, DI, Konfiguration, Protokollierung und Anwendungslebenszyklus.
Leistungen zu diesem Thema
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.
Technische Beratung und Design-Review
Wenn Sie DI, Lebenszeiten und die Aufteilung der Verantwortlichkeiten vor der Umsetzung klären möchten, können wir als technische Beratung und Designreview mit der Ausrichtung beginnen.
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.