Was macht Microsoft.Extensions Konfigurationsmuster?
Wann Sie diese Fähigkeit einsetzen sollten
Verwenden Sie diese Fähigkeit, wenn:
- Konfiguration aus appsettings.json an streng typisierte Klassen binden
- Konfiguration beim Anwendungsstart validieren (Fail Fast)
- Komplexe Validierungslogik für Einstellungen implementieren
- Konfigurationsklassen entwerfen, die testbar und wartbar sind
- IOptions, IOptionsSnapshot und IOptionsMonitor verstehen
Referenzdateien
- advanced-patterns.md: Validatoren mit Abhängigkeiten, benannte Optionen, vollständiges Produktionsbeispiel (AkkaSettings) und Testen von Validatoren
Warum Konfigurationsvalidierung wichtig ist
Das Problem: Anwendungen scheitern oft zur Laufzeit aufgrund von Fehlkonfigurationen – fehlende Verbindungszeichenfolgen, ungültige URLs, Werte außerhalb des zulässigen Bereichs. Diese Fehler treten tief in der Geschäftslogik auf, weit entfernt vom Ort, an dem die Konfiguration geladen wird.
Die Lösung: Konfiguration beim Start validieren. Wenn ungültig, sofort mit einer klaren Fehlermeldung fehlschlagen.
// SCHLECHT: Schlägt zur Laufzeit fehl, wenn jemand versucht, den Dienst zu nutzen
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// Löst 10 Minuten nach Produktionsstart eine NullReferenceException aus
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// GUT: Schlägt beim Start mit klarem Fehler fehl
// "SmtpSettings-Validierung fehlgeschlagen: Host ist erforderlich"
Muster 1: Basis-Optionsbindung
Eine Einstellungsklasse definieren
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
Aus der Konfiguration binden
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "[email protected]",
"Password": "secret",
"UseSsl": true
}
}
In Diensten konsumieren
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - Singleton, einmal beim Start gelesen
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
Muster 2: Datenannotations-Validierung
Für einfache Validierungsregeln Datenanmerkungen verwenden:
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP-Host ist erforderlich")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Port muss zwischen 1 und 65535 liegen")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "Benutzername muss eine gültige E-Mail-Adresse sein")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
Datenannotations-Validierung aktivieren
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // Attributbasierte Validierung aktivieren
.ValidateOnStart(); // Sofort beim Start validieren
Wichtiger Punkt: .ValidateOnStart() ist entscheidend. Ohne sie wird die Validierung nur ausgeführt, wenn zum ersten Mal auf die Optionen zugegriffen wird.
Muster 3: IValidateOptions für komplexe Validierung
Datenanmerkungen funktionieren für einfache Regeln, aber komplexe Validierung erfordert IValidateOptions<T>:
| Szenario | Datenanmerkungen | IValidateOptions |
|---|---|---|
| Erforderliches Feld | Ja | Ja |
| Bereichsprüfung | Ja | Ja |
| Eigenschaftsübergreifende Validierung | Nein | Ja |
| Bedingte Validierung | Nein | Ja |
| Externe Dienstprüfungen | Nein | Ja |
| Abhängigkeitsinjektion im Validator | Nein | Ja |
IValidateOptions implementieren
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.Host))
failures.Add("Host ist erforderlich");
if (options.Port is < 1 or > 65535)
failures.Add($"Port {options.Port} ist ungültig. Muss zwischen 1 und 65535 liegen");
// Eigenschaftsübergreifende Validierung
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
failures.Add("Passwort ist erforderlich, wenn ein Benutzername angegeben wird");
// Bedingte Validierung
if (options.UseSsl && options.Port == 25)
failures.Add("Port 25 wird normalerweise nicht mit SSL verwendet. Erwägen Sie Port 465 oder 587");
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
Den Validator registrieren
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();
Die Reihenfolge ist wichtig: Datenanmerkungen werden zuerst ausgeführt, dann IValidateOptions-Validatoren. Alle Fehlschläge werden zusammen erfasst.
Siehe advanced-patterns.md für Validatoren mit Abhängigkeiten, benannte Optionen und ein vollständiges Produktionsbeispiel.
Muster 4: Optionslebensdauer
| Schnittstelle | Lebensdauer | Lädt bei Änderung neu | Anwendungsfall |
|---|---|---|---|
IOptions<T> | Singleton | Nein | Statische Konfiguration, einmal gelesen |
IOptionsSnapshot<T> | Scoped | Ja (pro Anforderung) | Web-Apps, die aktuelle Konfiguration benötigen |
IOptionsMonitor<T> | Singleton | Ja (mit Rückruf) | Hintergrunddienste, Echtzeit-Updates |
IOptionsMonitor für Hintergrunddienste
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}
Muster 5: Nachkonfiguration
Optionen nach der Bindung, aber vor der Validierung ändern:
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
options.BaseUrl += '/';
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();
Anti-Patterns, die Sie vermeiden sollten
1. Manueller Konfigurationszugriff
// SCHLECHT: Umgeht Validierung, schwer zu testen
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // Keine Validierung!
}
}
// GUT: Streng typisiert, validiert
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // Beim Start validiert
}
}
2. Validierung im Konstruktor
// SCHLECHT: Validierung erfolgt zur Laufzeit, nicht beim Start
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Erforderlich fehlt"); // Zu spät!
}
}
// GUT: Validierung beim Start über IValidateOptions + ValidateOnStart()
3. ValidateOnStart vergessen
// SCHLECHT: Validierung wird nur ausgeführt, wenn zum ersten Mal darauf zugegriffen wird
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // ValidateOnStart fehlt!
// GUT: Schlägt sofort fehl, wenn ungültig
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();
4. Ausnahme in IValidateOptions werfen
// SCHLECHT: Wirft Ausnahme, unterbricht Validierungskette
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("Wert darf nicht negativ sein"); // Falsch!
return ValidateOptionsResult.Success;
}
// GUT: Fehlerergebnis zurückgeben
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("Wert darf nicht negativ sein");
return ValidateOptionsResult.Success;
}
Zusammenfassung
| Prinzip | Implementierung |
|---|---|
| Fail Fast | .ValidateOnStart() |
| Streng typisiert | An POCO-Klassen binden |
| Einfache Validierung | Datenanmerkungen |
| Komplexe Validierung | IValidateOptions<T> |
| Eigenschaftsübergreifende Regeln | IValidateOptions<T> |
| Umgebungsbewusst | IHostEnvironment injizieren |
| Testbar | Validatoren sind einfache Klassen |