Das Options-Muster in C#
[[academy-video-youtube({"vid": "ko1Ie9gDydY", "start_time": "0", "title": "The Options Pattern in C#", "creator": "Tim Corey", "length": "10m 15s"})]]
Konfigurationen in .NET-Anwendungen befinden sich typischerweise in appsettings.json, Umgebungsvariablen oder Benutzerschlüsseln. Diese Konfigurationsdaten aus einer Datei zu extrahieren und in Ihre Klassen in sauberer, testbarer Weise zu übertragen, ist das, was das Optionsmuster löst. Anstatt JSON-Schlüssel manuell zu lesen oder rohe Zeichenfolgen herumzureichen, binden Sie einen Konfigurationsabschnitt an eine stark typisierte C#-Klasse und lassen Sie die Abhängigkeitsinjektion sie dorthin liefern, wo sie benötigt wird.
In seinem Video "The Options Pattern in C#" führt Tim Corey die drei Variationen des Optionsmusters (IOptions, IOptionsSnapshot und IOptionsMonitor) ein, demonstriert jede in einer Blazor-Server-App und zeigt, wie man Validierung hinzufügt, damit Ihre Anwendung bei fehlender oder fehlerhafter Konfiguration schnell scheitert. Wenn Sie irgendein .NET-Projekt erstellen, das Konfigurationsdateien liest und Abhängigkeitsinjektionen verwendet, ist dieses Muster grundlegend.
T>## Das Setup: Ein POCO-Modell und appsettings.json
[0:34 - 1:46] Tim beginnt mit einer Blazor-Webanwendung (serverseitig gerendert, keine clientseitige Interaktivität), die bereits zwei Elemente an Ort und Stelle hat. Der erste ist ein Abschnitt in appsettings.json mit drei Schlüssel-Wert-Paaren unter einem CloudInfo Abschnitt:
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}
Das zweite Element ist eine einfache C#-Klasse (ein POCO), deren Eigenschaften die JSON-Schlüssel spiegeln:
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}
Tim weist darauf hin, dass die Konfigurationsquelle nicht speziell appsettings.json sein muss. Es könnte appsettings.Development.json, secrets.json oder eine beliebige Kombination sein. Das Optionsmuster liest von welchen Konfigurationsanbietern auch immer in der Anwendung registriert sind, und die Eigenschaftsnamen auf dem POCO entsprechen den JSON-Schlüsseln nach Konvention.
Registering Options in Program.cs
[2:25 - 3:15] Wiring the configuration to dependency injection takes two method calls in Program.cs:
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");
AddOptions<t>() registriert den Typ bei der DI. BindConfiguration("CloudInfo") teilt dem Framework mit, welchen Abschnitt der Konfiguration zuzuordnen ist. Der String "CloudInfo" entspricht dem JSON-Schlüssel in appsettings.json. Wenn der Abschnittsname und der Klassenname nicht übereinstimmen, verwendet das Framework das String-Argument, um die richtigen Daten zu finden.
IOptions: Der Singleton-Ansatz
[3:15 - 5:18] Mit der Registrierung an Ort und Stelle injiziert Tim die Konfiguration in eine Blazor-Seite unter Verwendung von IOptions<CloudInfoOptions>:
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}
Das entscheidende Detail ist, dass IOptions<t> als Singleton registriert ist. Die Konfigurationswerte werden einmal gelesen, wenn die Anwendung startet, und für die Lebensdauer des Prozesses zwischengespeichert. Wenn Sie appsettings.json ändern, während die App läuft, wird IOptions diese Änderungen nicht widerspiegeln.
Für die meisten Anwendungen ist dies die richtige Wahl. Die Konfiguration ändert sich selten zur Laufzeit, und die Singleton-Lebensdauer bedeutet null Overhead pro Anfrage.
IOptionsSnapshot: Gekapseltes Neuladen
[5:18 - 6:55] Tim tauscht IOptions gegen IOptionsSnapshot aus, um die gescoped Variante zu demonstrieren:
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig
IOptionsSnapshot<t> liest die Konfiguration frisch für jede Anfrage (gescoped Lebensdauer). Wenn Sie appsettings.json ändern, während die App läuft, wird die nächste Webanfrage die neuen Werte aufnehmen. Die vorherige Anfrage behält ihren eigenen Schnappschuss bei, sodass keine Inkonsistenz während der Anfrage besteht.
Dies ist nützlich für Anwendungen, bei denen Konfigurationsänderungen in Kraft treten müssen, ohne dass ein Neustart erforderlich ist, z.B. beim Umschalten von Funktionsflags, Aktualisieren von API-Endpunkten oder Drehen von Speicher-Verbindungszeichenfolgen. Der Kompromiss besteht in einem kleinen Aufwand pro Anfrage, um die Konfiguration erneut zu lesen, was für die meisten Workloads vernachlässigbar ist.
IOptionsMonitor: Live-Änderungsbenachrichtigungen
[6:55 - 8:34] Die dritte Variante, IOptionsMonitor<t>, geht weiter als das Snapshot-Reloading. Es verfolgt aktiv Konfigurationsänderungen und kann Rückrufe auslösen, wenn sich Werte aktualisieren:
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}
Anders als IOptionsSnapshot, das nur zwischen Anfragen aktualisiert, kann IOptionsMonitor Änderungen während einer lang laufenden Operation erkennen. Tim weist darauf hin, dass dies vor allem für Hintergrunddienste, SignalR-Hubs oder Komponenten mit einer längeren Lebensdauer als eine einzelne HTTP-Anfrage relevant ist.
Hinzufügen von Validierung
[8:34 - 10:05] The final piece Tim covers is validation. Das Optionsmuster unterstützt Inline-Validierungsregeln, die ablaufen, wenn die Konfiguration erstmals aufgerufen wird:
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));
Tim demonstriert den Fehlerfall, indem er den Storage-Wert aus appsettings.json entfernt und die Anwendung startet. Das Ergebnis ist eine sofortige nicht behandelte Ausnahme: "CloudInfoOptions failed validation." Die Anwendung weigert sich zu starten, wenn die Konfiguration unvollständig ist - genau das Verhalten, das Sie wünschen. Das Entdecken eines fehlenden Wertes beim Start ist weitaus besser, als auf einen Nullverweis in der Produktion zu stoßen um 2 Uhr morgens.
Für komplexere Validierungsszenarien (übergreifende Prüfungen, bedingte Regeln) können Sie mehrere .Validate()-Aufrufe verketten oder IValidateOptions<t> als eine dedizierte Validierungsklasse implementieren.
Zusammenfassung: Drei Schnittstellen, ein Muster
[10:05 - 10:10] Das Optionsmuster bietet eine klare Trennung zwischen Konfigurationsspeicherung und -nutzung. IOptions deckt die meisten Anwendungsfälle ab, in denen die Konfiguration statisch ist. IOptionsSnapshot handhabt Anwendungen, die Änderungen zwischen Anfragen aufnehmen müssen. IOptionsMonitor dient lang laufenden Komponenten, die ein Echtzeitbewusstsein für Konfigurationsaktualisierungen benötigen. Und die Validierung stellt sicher, dass Ihre Anwendung lautstark fehlschlägt, wenn erforderliche Werte fehlen.
Abschluss
[10:10 - 10:15] Zusammengefasst: Registrieren Sie Ihre Konfiguration mit AddOptions<t>().BindConfiguration("SectionName") in Program.cs, injizieren Sie eine der drei Schnittstellen (IOptions, IOptionsSnapshot oder IOptionsMonitor), abhängig von Ihren Anforderungen an das Nachladen, und fügen Sie .Validate() hinzu, um fehlende Werte beim Start anstelle zur Laufzeit abzufangen.
Das Muster funktioniert in jedem .NET-Projekt: Blazor, ASP.NET Core, Konsolenanwendungen, Worker-Dienste. Die Registrierung ist überall gleich.
Beispiel Tipp: Wenn Sie unsicher sind, welche Schnittstelle Sie verwenden sollen, beginnen Sie mit IOptions<t>. Es ist das einfachste, hat keinen Overhead pro Anfrage und deckt die meisten Fälle ab, in denen die Konfiguration beim Start eingestellt wird und sich nicht ändert. Wechseln Sie nur zu IOptionsSnapshot oder IOptionsMonitor, wenn Sie einen konkreten Bedarf für das Nachladen zur Laufzeit haben.
Sehen Sie sich das vollständige Video auf seinem YouTube Kanal an, um noch mehr Einblicke in C#-Konfigurationsmuster zu gewinnen.
