Das Optionsmuster verwendet Klassen, um stark typisierten Zugriff auf zusammengehörige Einstellungsgruppen zu ermöglichen. Wenn Konfigurationseinstellungen nach Szenario in separate Klassen isoliert werden, entspricht die Anwendung zwei wichtigen Prinzipien der Softwareentwicklung:
Trennung von Bereichen: Einstellungen für die verschiedenen Teile der Anwendung hängen weder voneinander ab, noch sind sie miteinander gekoppelt.
Optionen bieten auch einen Mechanismus, um Konfigurationsdaten zu validieren. Weitere Informationen finden Sie im Abschnitt Optionsvalidierung.
Binden von hierarchischen Konfigurationsdaten
Die bevorzugte Methode für das Lesen zugehöriger Konfigurationswerte ist die Verwendung des Optionsmusters. Das Optionsmuster wird durch die IOptions<TOptions>-Schnittstelle ermöglicht, bei der generische Typparameter TOptions auf class eingeschränkt ist. IOptions<TOptions> kann später über die Abhängigkeitsinjektion (Dependency Injection, DI) bereitgestellt werden. Weitere Informationen finden Sie unter Abhängigkeitsinjektion in .NET.
So lesen Sie beispielsweise die hervorgehobenen Konfigurationswerte aus einer appsettings.json-Datei:
Erstellen Sie die folgende neue TransientFaultHandlingOptions-Klasse:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
Wenn Sie das Optionsmuster verwenden, gelten für eine Optionsklasse folgende Bedingungen:
Sie darf nicht abstrakt sein und muss über einen öffentlichen parameterlosen Konstruktor verfügen.
Enthalten von öffentlichen Lese-/Schreibeigenschaften für Bindungen (Felder sind nicht gebunden)
Der folgende Code ist Teil der C#-Datei Program.cs und:
Ruft ConfigurationBinder.Bind auf, um die TransientFaultHandlingOptions-Klasse an den "TransientFaultHandlingOptions"-Abschnitt zu binden.
Zeigt die Konfigurationsdaten an.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Bind(options);
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
// <Output>
// Sample output:
Im vorherigen Code ist der "TransientFaultHandlingOptions"-Abschnitt der JSON-Konfigurationsdatei an die TransientFaultHandlingOptions-Instanz gebunden. Dadurch werden die Eigenschaften von C#-Objekten mit den entsprechenden Werten aus der Konfiguration versorgt.
ConfigurationBinder.Get<T> bindet den angegebenen Typ und gibt ihn zurück. ConfigurationBinder.Get<T> ist möglicherweise praktischer als die Verwendung von ConfigurationBinder.Bind. Der folgende Code zeigt die Verwendung von ConfigurationBinder.Get<T> mit der TransientFaultHandlingOptions-Klasse:
var options =
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Get<TransientFaultHandlingOptions>();
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
Im vorherigen Code wird ConfigurationBinder.Get<T> verwendet, um eine Instanz des TransientFaultHandlingOptions-Objekts mit seinen aus der zugrunde liegenden Konfiguration aufgefüllten Eigenschaftswerten zu erhalten.
Eine alternative Vorgehensweise bei der Verwendung des Optionsmusters besteht darin, den Abschnitt "TransientFaultHandlingOptions" zu binden und ihn dem Dienstcontainer für die Abhängigkeitsinjektion hinzuzufügen. Im folgenden Code wird TransientFaultHandlingOptions mit Configure zum Dienstcontainer hinzugefügt und an die Konfiguration gebunden:
Der key-Parameter entspricht dem Namen des zu suchenden Konfigurationsabschnitts. Er muss nicht mit dem Namen des Typs übereinstimmen, der ihn darstellt. Beispiel: Ein Abschnitt namens "FaultHandling" wird durch die Klasse TransientFaultHandlingOptions dargestellt. In diesem Fall übergeben Sie "FaultHandling" an die Funktion GetSection. Der nameof-Operator wird verwendet, wenn der benannte Abschnitt mit dem Typ übereinstimmt, dem er entspricht.
Mithilfe des vorangehenden Codes liest der folgende Code die Positionsoptionen:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
Im vorangehenden Code werden Änderungen an der JSON-Konfigurationsdatei nach dem Start der App nicht gelesen. Nach dem Start der App auftretende Änderungen lesen Sie mit IOptionsSnapshot oder IOptionsMonitor, um Änderungen zu überwachen, während sie auftreten, und entsprechend zu reagieren.
Die Verwendung eines generischen Wrappertyps bietet Ihnen die Möglichkeit, die Lebensdauer der Option vom DI-Container (Dependency Injection) zu entkoppeln. Die IOptions<TOptions>.Value-Schnittstelle stellt für Ihren Optionstyp eine Abstraktionsebene einschließlich allgemeiner Einschränkungen bereit. Dies bietet folgende Vorteile:
Die Auswertung der T-Konfigurationsinstanz wird auf den Zugriff von IOptions<TOptions>.Value verschoben und nicht zum Zeitpunkt der Injektion durchgeführt. Dies ist wichtig, weil Sie die Option T an verschiedenen Stellen nutzen und die Lebensdauersemantik auswählen können, ohne T zu verändern.
Beim Registrieren von Optionen des Typs T müssen Sie den Typ T nicht explizit registrieren. Dies ist beim Erstellen einer Bibliothek mit einfachen Standardeinstellungen hilfreich, wenn Sie den Aufrufer nicht zum Registrieren von Optionen im DI-Container mit einer bestimmten Lebensdauer zwingen möchten.
Aus Sicht der API sind hierdurch Einschränkungen für den Typ T möglich (in diesem Fall ist T auf einen Verweistyp beschränkt).
Verwenden von IOptionsSnapshot zum Lesen aktualisierter Daten
Bei Verwendung von IOptionsSnapshot<TOptions> werden Optionen einmal pro Anforderung (beim Zugriff) berechnet. Anschließend werden sie für die Lebensdauer der Anforderung zwischengespeichert. Änderungen an der Konfiguration werden gelesen, nachdem die App gestartet wurde, wenn Konfigurationsanbieter verwendet werden, die das Lesen aktualisierter Konfigurationswerte unterstützen.
Der Unterschied zwischen IOptionsMonitor und IOptionsSnapshot ist folgender:
IOptionsMonitor ist ein Singleton-Dienst, der zu jeder Zeit aktuelle Optionswerte empfängt, was insbesondere bei Singleton-Abhängigkeiten nützlich ist.
IOptionsSnapshot ist ein bereichsbezogener Dienst und bietet eine Momentaufnahme der Optionen zu dem Zeitpunkt, da das IOptionsSnapshot<T>-Objekt konstruiert wird. Momentaufnahmen von Optionen sind für die Verwendung mit vorübergehenden und bereichsbezogenen Abhängigkeiten bestimmt.
Im vorherigen Code wird die Configure<TOptions>-Methode verwendet, um eine Konfigurationsinstanz zu registrieren, an die TOptions gebunden wird, und die Optionen werden aktualisiert, wenn sich die Konfiguration ändert.
IOptionsMonitor
Der IOptionsMonitor Typ unterstützt Änderungsbenachrichtigungen und ermöglicht Szenarien, in denen Ihre App möglicherweise dynamisch auf Änderungen der Konfigurationsquelle reagieren muss. Dies ist nützlich, wenn Sie nach dem Starten der App auf Änderungen in Konfigurationsdaten reagieren müssen. Änderungsbenachrichtigungen werden nur für dateisystembasierte Konfigurationsanbieter unterstützt, z. B. für die folgenden:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
public void DisplayValues()
{
TransientFaultHandlingOptions options = monitor.CurrentValue;
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
Im vorangehenden Code werden Änderungen an der JSON-Konfigurationsdatei nach dem Start der App gelesen.
Tipp
Einige Dateisysteme, z.B. Docker-Container und Netzwerkfreigaben, senden Änderungsmeldungen möglicherweise nicht zuverlässig . Wenn Sie das IOptionsMonitor<TOptions> Interface in diesen Umgebungen verwenden, legen Sie die DOTNET_USE_POLLING_FILE_WATCHER Umgebungsvariable auf 1oder true fest, um das Dateisystem auf Änderungen hin abzufragen. Das Intervall, in dem Änderungen abgefragt werden, beträgt alle vier Sekunden und ist nicht konfigurierbar.
Anstatt zwei Klassen für die Bindung von Features:Personalize und Features:WeatherStation zu erstellen, wird folgende Klasse für jeden Abschnitt verwendet:
public class Features
{
public const string Personalize = nameof(Personalize);
public const string WeatherStation = nameof(WeatherStation);
public bool Enabled { get; set; }
public string ApiKey { get; set; }
}
Der folgende Code dient zum Konfigurieren der benannten Optionen:
Der folgende Code zeigt die benannten Optionen an:
public sealed class Service
{
private readonly Features _personalizeFeature;
private readonly Features _weatherStationFeature;
public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
{
_personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
_weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
}
}
Alle Optionen sind benannte Instanzen. IConfigureOptions<TOptions>-Instanzen werden behandelt, als würden sie die Options.DefaultName-Instanz anzielen. Diese ist string.Empty. IConfigureNamedOptions<TOptions> implementiert auch IConfigureOptions<TOptions>. Die standardmäßige Implementierung von IOptionsFactory<TOptions> verfügt über eine Logik, um jede einzelne entsprechend zu verwenden. Die benannte Option null wird verwendet, um auf alle benannten Instanzen anstelle einer bestimmten benannten Instanz abzuzielen. ConfigureAll und PostConfigureAll verwenden diese Konvention.
OptionsBuilder-API
OptionsBuilder<TOptions> dient zum Konfigurieren von TOptions-Instanzen. OptionsBuilder optimiert die Erstellung von benannten Optionen, da es sich nur um einen einzelnen Parameter für den ersten AddOptions<TOptions>(string optionsName)-Aufruf handelt statt um alle nachfolgenden Aufrufe. Die Optionsvalidierung und die ConfigureOptions-Überladungen, die Dienstabhängigkeiten akzeptieren, sind nur über OptionsBuilder verfügbar.
Verwenden von DI-Diensten zum Konfigurieren von Optionen
Wenn Sie Optionen konfigurieren, können Sie die Dependency Injection verwenden, um auf registrierte Dienste zuzugreifen und für das Konfigurieren von Optionen zu verwenden. Dies ist nützlich, wenn Sie auf Dienste zugreifen müssen, um Optionen zu konfigurieren. Auf Dienste kann über DI zugegriffen werden, während Optionen auf zwei Arten konfiguriert werden:
Übergeben eines Konfigurationsdelegaten an Konfigurieren auf OptionsBuilder<TOptions>OptionsBuilder<TOptions> stellt Überladungen von Configure zur Verfügung, die es ermöglichen, bis zu fünf Dienste zu verwenden, um Optionen zu konfigurieren:
Es wird empfohlen, einen Konfigurationsdelegaten an Configure zu übergeben, da das Erstellen eines Diensts komplexer ist. Das Erstellen eines Typs entspricht den Aktionen des Frameworks beim Aufruf von Configure. Wenn Konfigurieren aufgerufen wird, wird eine vorübergehende, generische IConfigureNamedOptions<TOptions> registriert, die über einen Konstruktor verfügt, der die angegeben generischen Diensttypen akzeptiert.
Überprüfung von Optionen
Bei der Überprüfung der Optionen können Optionswerte überprüft werden.
Sehen Sie sich die nachfolgende Datei appsettings.json an:
Die folgende Klasse erstellt eine Bindung zum "MyCustomSettingsSection"-Konfigurationsabschnitt und wendet einige DataAnnotations-Regeln an:
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int Scale { get; set; }
[Required]
public required int VerbosityLevel { get; set; }
}
In der vorangehenden SettingsOptions-Klasse enthält die Eigenschaft ConfigurationSectionName den Namen des Konfigurationsabschnitts, an die er gebunden werden soll. In diesem Szenario stellt das Optionsobjekt den Namen seines Konfigurationsabschnitts zur Verfügung.
Tipp
Der Name des Konfigurationsabschnitts ist unabhängig vom Konfigurationsobjekt, an das er gebunden ist. Anders ausgedrückt: Ein Konfigurationsabschnitt mit dem Namen "FooBarOptions" kann an ein Optionsobjekt namens ZedOptions gebunden werden. Obwohl es üblich ist, diese beiden Elemente gleich zu benennen, ist dies nicht notwendig und kann tatsächlich Namenskonflikte verursachen.
Der folgende Code zeigt die Konfigurationswerte an oder meldet Überprüfungsfehler:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ValidationService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<SettingsOptions> _config;
public ValidationService(
ILogger<ValidationService> logger,
IOptions<SettingsOptions> config)
{
_config = config;
_logger = logger;
try
{
SettingsOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures)
{
_logger.LogError("Validation error: {FailureMessage}", failure);
}
}
}
}
Der folgende Code wendet mithilfe eines Delegaten eine komplexere Überprüfungsregel an:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
Die Überprüfung erfolgt zur Laufzeit, sie kann jedoch so konfiguriert werden, dass sie beim Start erfolgt, indem Sie stattdessen einen Aufruf an ValidateOnStart anhängen:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.")
.ValidateOnStart();
Legen Sie die Postkonfiguration mit IPostConfigureOptions<TOptions> fest. Die Postkonfiguration wird ausgeführt, nachdem die gesamte IConfigureOptions<TOptions>-Konfiguration erfolgt ist. Sie kann in Szenarien nützlich sein, in denen die Konfiguration außer Kraft gesetzt werden soll:
Die Quelle für diesen Inhalt finden Sie auf GitHub, wo Sie auch Issues und Pull Requests erstellen und überprüfen können. Weitere Informationen finden Sie in unserem Leitfaden für Mitwirkende.
Feedback zu .NET
.NET ist ein Open Source-Projekt. Wählen Sie einen Link aus, um Feedback zu geben:
Verstehen und Implementieren der Abhängigkeitsinjektion in einer ASP.NET Core-App. Verwenden Sie den integrierten Dienstcontainer von Core ASP.NET, um Abhängigkeiten zu verwalten. Registrieren Sie Dienste für den Dienstcontainer.