19. Sep 2022
Lesedauer 7 Min.
One Host to rule them all
Microsoft.Extensions.Hosting
Logging, Konfiguration und Dependency Injection: beim Generic Host alles inklusive.

Schon mit .NET Core 1 und ASP.NET Core hat Microsoft den Startvorgang einer Anwendung im Webbereich für .NET revolutioniert: ein einfacher Host, der in der Lage ist, Dependency Injection, Konfiguration und Bootstrapping der Anwendung zu vereinheitlichen. Mittlerweile ist dies zum allgegenwärtigen Pattern geworden. Seit .NET Core 3 liefert uns Microsoft das Generic Host Framework, ein Paket, um alle Anwendungsarten abzudecken. Damit bekommen wir die Grundfeatures einer modernen Anwendung für alle Lebenslagen.
Microsoft.Extensions.Hosting
Das Paket Microsoft.Extensions.Hosting, das ASP.NET Core zum Beispiel automatisch verwendet, liefert die Grundinfrastruktur für unsere Anwendung. Dieses Paket ist mit über 200 Millionen Downloads kein unbekanntes. Was oft vergessen wird, ist jedoch, dass dieses Paket in jedem Anwendungstyp verwendet werden kann, egal ob Konsolenanwendung oder klassische Desktop-Anwendung wie Windows Forms oder WPF.Da dieses Paket sogar für .NET Framework und .NET Standard verfügbar ist, erweitert sich die Palette auf alle Anwendungstypen – egal ob es sich nun um .NET, .NET Core oder .NET Framework (Bild 1) handelt.
Microsoft.Extensions.Hosting:Für alle Anwendungstypen verfügbar(Bild 1)
Autor
Die Installation
Für den weiteren Verlauf entsteht ein einfaches Beispiel, damit wir die Möglichkeiten des Generic Hosts kennenlernen:<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include=
"Microsoft.Extensions.Hosting" Version="6.0.1" />
</ItemGroup>
</Project>
Es handelt sich um eine einfache Konsolenanwendung, die per NuGet das Grundpaket Microsoft.Extensions.Hosting installiert hat. Ab diesem Zeitpunkt kann es losgehen mit dem Host an sich. Der Host ist das Startobjekt, mit dem man die Grundinfrastruktur, die man erhält, weiter verfeinern und konfigurieren kann, um anschließend den Host zu starten. Dieser wird für gewöhnlich einmalig gestartet und läuft anschließend bis zum Beenden der Anwendung.
Der HostBuilder
Microsoft hat sich seit der ersten Version von .NET Core in das Builder Pattern verliebt. Dieses ist an vielen Stellen anzutreffen, so auch beim Generic Host. Es wird nämlich nicht einfach ein Host-Objekt instanziert, sondern es muss mit dem HostBuilder gearbeitet werden:using Microsoft
.Extensions.Hosting;
IHost host =
new HostBuilder()
.Build();
host.Start();
host.WaitForShutdown();
Der HostBuilder ermöglicht die Konfiguration des Hosts an sich, und am Ende kann mittels der Build-Methode ein Host erzeugt werden. Es bleibt einem selbst überlassen, ob man den Host anschließend synchron oder auch asynchron starten lassen möchte. Dabei hat man die Option, dass der Aufruf blockiert, bis die Anwendung ein Herunterfahren anfordert. Es bietet sich in der Konsole zum Beispiel die Methode WaitForShutdown an, da diese auf [Strg]+[C] reagiert, was in der Konsole zum Beenden der Anwendung führt.
Logging: Out of the Box
Mit dem Microsoft.Extensions.Hosting-Paket kommen automatisch eine Vielzahl an Abhängigkeiten sofort in die Anwendung hinein. Dies wird in Visual Studio deutlich, wenn man sich das Paket einmal näher ansieht (Bild 2).
Viele Abhängigkeiten, aber auch viele Features:Das Paket Microsoft.Extensions.Hosting im Solution Explorer(Bild 2)
Autor
Neben der Konfiguration erhält man auch Dependency Injection. Sie können also all diese Features sofort nutzen. Möchte man nun, dass die Anwendung mit dem Standard-Logmechanismus von Microsoft arbeitet, so muss hierfür nur die Methode ConfigureLogging aufgerufen werden.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
IHost host = new HostBuilder()
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.AddDebug();
loggingBuilder.AddConsole();
})
.Build();
host.Start();
host.WaitForShutdown();
Diese Methode liefert dabei einen LoggingBuilder (erneut das Builder-Pattern), mit dem man das eigene Logsystem konfigurieren kann, wie auch immer man möchte. Im Standard kann man direkt in das Debug-Fenster von Visual Studio loggen, oder auch in die Konsole. Erweiterungen gibt es hierfür mittlerweile wie Sand am Meer. Egal ob in eine Datei oder einen externen Loggingdienst – man hat heute die Qual der Wahl, wie man sein Logging gestalten möchte. Bereits mit dieser Grundkonfiguration wirft uns die Konsole beim nächsten Start bereits einige Zeilen entgegen (Bild 3).

Logging-Ausgabeauf der Konsole(Bild 3)
Autor
Wir sind also damit bereits in der Lage, die Anwendung hochzufahren, und diese könnte bereits munter vor sich hin loggen, wenn sie denn etwas tun würde.
Configuration: Out of the Box
Was wäre eine Anwendung ohne Konfiguration? War es früher die App.config oder Web.config, ist es heute – ja, was denn nun eigentlich? Man hat auch hier die Qual der Wahl. Bei der Konfiguration unterscheidet Microsoft zwischen zwei Phasen des Startvorgangs: Konfiguration, die für das Hochfahren der Anwendung kritisch ist, wie etwa das Content-Verzeichnis, und im späteren Verlauf die Anwendungskonfiguration, die erst nach Anwendungsstart notwendig wird.using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
IHost host = new HostBuilder()
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.AddDebug();
loggingBuilder.AddConsole();
})
.ConfigureAppConfiguration(configurationBuilder =>
{
})
.ConfigureHostConfiguration(configurationBuilder =>
{
})
.Build();
host.Start();
host.WaitForShutdown();
Dies ist auch der Grund, warum es hierfür zwei Methoden gibt: einerseits die AppConfiguration, andererseits die HostConfiguration. Erneut erhält man auch hier einen Builder für die Configuration. ASP.NET Core arbeitet dabei sehr häufig mit der AppSettings.json. Diese ist im Generic Host so noch nicht konfiguriert. Will man also hier JSON-basiert eine Konfiguration erhalten, muss dies erst selbst definiert werden.
.ConfigureHostConfiguration(configurationBuilder => {
configurationBuilder.AddJsonFile(
"MyAppSettings.json", false);
configurationBuilder.AddJsonFile($"MyAppSettings
.{Environment.MachineName}.json", false);
configurationBuilder.AddJsonFile($"MyAppSettings
.{Environment.UserName}.json", false);
configurationBuilder
.AddEnvironment-
Variables();
configurationBuilder
.AddCommandLine(
args);
})
Man kann dem ConfigurationBuilder mitgeben, dass man gerne eine JSON-Datei inkludieren möchte, wie zum Beispiel eine MyAppSettings.json. Dabei kann mit einem Boole-Wert angegeben werden, ob diese Datei optional ist. Wichtig dabei ist, dass die Aufrufe kaskadierend sind. Spätere Aufrufe überschreiben vorhandene Werte – zum Glück!Das bedeutet, wir können eine Grundkonfiguration in der Datei MyAppSettings.json vorgeben, diese kann aber überschrieben werden. In unserem Fall passiert das mittels zwei weiteren JSON-Dateien, die vom Maschinennamen oder vom Benutzernamen abhängen, wobei der Benutzername den Maschinennamen schlägt. Ebenfalls gleich verhält es sich mit den Umgebungsvariablen und am Ende den Befehlszeilenargumenten.
Dependency Injection: Out of the Box
Neben Logging und Configuration ist auch Dependency Injection mit von der Partie..ConfigureServices(services =>
{
services.AddSingleton<MyClass>();
})
Mit einem einfachen Aufruf von ConfigureServices können die Dienste, die von der Anwendung benötigt werden, dem Dependency Injection Container bekannt gemacht werden. Verwendet wird auch hier wieder das Standardframework von Microsoft, Microsoft.Extensions.DependencyInjection (siehe [1]).
Konfiguration, Logging, Dependency Injection: Und was nun?
Nun hat man also ganz viele tolle Sachen „out of the Box“. Trotzdem bleibt die finale Frage unbeantwortet: Was macht man nun? Der Generic Host arbeitet dabei mit dem aus seiner Sicht wichtigsten Interface, dem IHostedService. Dies ist eine Klasse, welche dem Lebenszyklus des Hosts unterworfen ist. Das bedeutet: Beim Starten des Hosts werden alle ihm bekannten HostedServices gestartet, und beim Herunterfahren werden diese wieder beendet.Dabei müssen lediglich zwei Dinge beachtet werden. Zum Beginn muss die Registrierung des HostedService vorgenommen werden:.ConfigureServices(services =>
{
services.AddSingleton<MyClass>();
services.AddHostedService<MyHostedClass>();
})
Nach erfolgter Registrierung muss die Klasse selbst (in unserem Fall die MyHostedClass) implementiert werden. Dabei hat man die Wahl, enweder auf das Interface IHostedService zurückzugreifen oder auf die abstrakte Basisklasse BackgroundService.
class MyHostedClass : IHostedService
{
public Task StartAsync(
CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task StopAsync(
CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
Das Interface definiert dabei, dass es zwei Methoden geben muss: eine Start- und eine Stoppmethode für das Beenden des Dienstes. Der Name Dienst/Service ist dabei so zu verstehen, dass er etwas ist, was für die Anwendungsdauer läuft. Es ist dabei irrelevant, ob es sich nun um einen Webserver handelt, einen TCP-Server oder eine Desktop-Anwendung, die im Start das Hauptfenster erzeugt und anzeigt.Wichtig zu wissen ist an diesem Punkt noch, dass es mehrere HostedServices geben kann. Diese werden in der Reihenfolge der Registrierung der Reihe nach gestartet und auch in derselben Reihenfolge wieder heruntergefahren.
Setzen wir die Puzzlesteine zusammen!
Will man nun sämtliche Vorzüge des Generic Hosts nutzen, so können wir mit all den Dingen, die wir bis jetzt zusammengebaut haben, die finale Software starten. In unserer Klasse MyHostedService können wir alle notwendigen Dinge per Dependency Injection abrufen und verwenden.public class MyHostedClass : IHostedService
{
private readonly IConfiguration _configuration;
private readonly ILogger<MyHostedClass> _logger;
private readonly IHostApplication
Lifetime _hostApplicationLifetime;
public MyHostedClass(
IConfiguration configuration,
ILogger<MyHostedClass> logger,
IHostApplicationLifetime
hostApplicationLifetime)
{
_configuration = configuration;
_logger = logger;
_hostApplicationLifetime = hostApplicationLifetime;
}
public Task StartAsync(
CancellationToken cancellationToken)
{
Task.Delay(4_000).ContinueWith(
_ => _hostApplicationLifetime.StopApplication());
return Task.CompletedTask;
}
}Bei diesem Beispiel injizieren wir über den Konstruktor die Konfiguration, den Logger und eine weitere Klasse vom Typ IHostApplicationLifetime. Damit erhalten wir ein Steuerungsobjekt, das wir für das Herunterfahren der gesamten Anwendung nutzen können. In der Start-Methode wurde als Beispiel ein Task.Delay verwendet, das nach vier Sekunden den Stop der Anwendung verursacht, was man gut in unserer Konsole sehen kann (Bild 4).

Nach vier Sekundenist Schluss: Die Task wird beendet(Bild 4)
Autor
Wichtig und auch gewollt ist, dass wir den Task.Delay nicht mittels await abwarten, da ansonsten der Startvorgang nicht abgeschlossen werden kann, was zu unerwünschten Nebeneffekten führen kann. Hilfreich ist es also, dass wir den Task im Fire-and-forget-Modus starten und bewusst nicht auf ihn warten – gerade für manche Anwendungen gut zu wissen.
Fazit
Microsoft hat ganze Arbeit geleistet: Die Grundanforderungen wie Configuration, Logging und Dependency Injection sind heute Werkzeuge, die kaum jemand mehr missen möchte. Der Generic Host verbindet all diese und ermöglicht damit, Anwendungen mittels wenig Code aufzusetzen und zu starten. Für viele Projekte ein absoluter Mehrwert.Fussnoten
- Christian Giesswein, Klein, kleiner, winzig, dotnetpro 10/2021, Seite 56 f., http://www.dotnetpro.de/A2110NETirol