Wzorzec opcji w C#
[[academy-video-youtube({"vid": "ko1Ie9gDydY", "start_time": "0", "title": "The Options Pattern in C#", "creator": "Tim Corey", "length": "10m 15s"})]]
Konfiguracja w aplikacjach .NET zwykle znajduje się w appsettings.json, zmiennych środowiskowych lub tajnych danych użytkownika. Wydobywanie tych danych konfiguracyjnych z pliku i umieszczanie ich w twoich klasach w czysty, testowalny sposób to coś, co rozwiązuje wzorzec opcji. Zamiast ręcznie czytać klucze JSON lub przekazywać surowe ciągi znaków, wiążesz sekcję konfiguracji z silnie typowaną klasą C# i pozwalasz na wstrzykiwanie zależności, aby dostarczyć ją wszędzie tam, gdzie jest potrzebna.
W swoim wideo "The Options Pattern in C#", Tim Corey wprowadza trzy warianty wzorca opcji (IOptions, IOptionsSnapshot i IOptionsMonitor), demonstruje każdy z nich w aplikacji serwerowej Blazor i pokazuje, jak dodać walidację, aby aplikacja szybko się zamknęła, gdy konfiguracja jest niekompletna lub niepoprawna. Jeśli tworzysz jakikolwiek projekt .NET, który czyta pliki konfiguracyjne i używa wstrzykiwania zależności, ten wzorzec jest podstawowy.
Przygotowanie: Model POCO i appsettings.json
[0:34 - 1:46] Tim zaczyna od aplikacji webowej Blazor (renderowanej po stronie serwera, bez interaktywności po stronie klienta), która już ma dwie części na miejscu. Pierwszym jest sekcja w appsettings.json z trzema parami klucz-wartość w sekcji CloudInfo:
// 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"
}
}Druga część to zwykła klasa C# (POCO), której właściwości odzwierciedlają klucze JSON:
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 zauważa, że źródłem konfiguracji nie musi być konkretnie appsettings.json. Może to być appsettings.Development.json, secrets.json lub dowolna kombinacja. Wzorzec opcji czyta z dowolnych dostawców konfiguracji zarejestrowanych w aplikacji, a nazwy właściwości na POCO pasują do kluczy JSON z konwencji.
Rejestracja Opcji w Program.cs
[2:25 - 3:15] Podłączenie konfiguracji do iniekcji zależności wymaga dwóch wywołań metod w 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>() rejestruje typ w DI. BindConfiguration("CloudInfo") informuje framework, którą część konfiguracji mapować. String "CloudInfo" odpowiada kluczowi JSON w appsettings.json. Jeśli nazwa sekcji i nazwa klasy nie pasują, argument ciągu znaków jest tym, co framework używa do znalezienia odpowiednich danych.
IOptions: Podejście Singleton
[3:15 - 5:18] Po rejestracji, Tim wprowadza konfigurację na stronę Blazor używając 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
}
}Kluczowy szczegół to, że IOptions<t> jest zarejestrowany jako singleton. Wartości konfiguracji są odczytywane raz, gdy aplikacja się uruchamia, i są przechowywane w pamięci podręcznej na czas trwający cały proces. Jeśli zmienisz appsettings.json podczas działania aplikacji, IOptions nie odzwierciedli tych zmian.
Dla większości aplikacji to właściwy wybór. Konfiguracja rzadko zmienia się w czasie wykonywania, a czas życia singletona oznacza brak dodatkowego obciążenia na żądanie.
IOptionsSnapshot: Odświeżanie Zasięgu
[5:18 - 6:55] Tim zamienia IOptions na IOptionsSnapshot, aby zademonstrować wariant o zasięgu:
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig@inject IOptionsSnapshot<CloudInfoOptions> CloudConfigIOptionsSnapshot<t> czyta konfigurację świeżo dla każdego żądania (czas życia o zasięgu). Jeśli zmodyfikujesz appsettings.json podczas działania aplikacji, kolejne żądanie internetowe przechwyci nowe wartości. Poprzednie żądanie zachowuje własną migawkę, więc nie ma niezgodności w trakcie żądania.
To jest przydatne w aplikacjach, gdzie zmiany konfiguracji muszą być wdrożone bez ponownego uruchamiania, jak przełączanie flag funkcjonalnych, aktualizowanie punktów końcowych API, czy rotacja ciągów połączeń do przechowywania. Kompromisem jest niewielki koszt na żądanie związany z ponownym odczytem konfiguracji, który jest pomijalny dla większości obciążeń.
IOptionsMonitor: Powiadomienia o Zmianach na Żywo
[6:55 - 8:34] Trzeci wariant, IOptionsMonitor<t>, idzie dalej niż przeładowanie przyrostowe. Aktywnie monitoruje zmiany w konfiguracji i może uruchamiać callbacki, gdy wartości się zmienią:
@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
});
}
}W przeciwieństwie do IOptionsSnapshot, który odświeża tylko między żądaniami, IOptionsMonitor może wykrywać zmiany podczas długotrwałej operacji. Tim wskazuje, że ma to największe znaczenie dla usług działających w tle, hubów SignalR lub dowolnego komponentu o dłuższym czasie życia niż pojedyncze żądanie HTTP.
Dodawanie Walidacji
[8:34 - 10:05] The final piece Tim covers is validation. Wzorzec opcji obsługuje zasady walidacji inline, które działają podczas pierwszego dostępu do konfiguracji:
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 demonstruje przypadek niepowodzenia, usuwając wartość Storage z appsettings.json i uruchamiając aplikację. Wynik to natychmiastowy nieobsługiwany wyjątek: "CloudInfoOptions nie przeszło walidacji." Aplikacja odmawia rozpoczęcia z niekompletną konfiguracją, co jest dokładnie zachowaniem, jakiego oczekujesz. Odkrycie brakującej wartości przy starcie jest znacznie lepsze niż uderzenie w referencję z zerowym w produkcji o 2 w nocy.
W przypadku bardziej złożonych scenariuszy walidacji (kontrole między właściwościami, reguły warunkowe), można łańcuchowo połączyć wiele wywołań .Validate() lub wdrożyć IValidateOptions<t> jako dedykowaną klasę walidacji.
Podsumowanie: Trzy Interfejsy, Jeden Wzorzec
[10:05 - 10:10] Wzorzec opcji zapewnia czyste oddzielenie między magazynowaniem konfiguracji a jej konsumowaniem. IOptions obejmuje większość przypadków użycia, gdzie konfiguracja jest statyczna. IOptionsSnapshot obsługuje aplikacje, które muszą odbierać zmiany między żądaniami. IOptionsMonitor obsługuje komponenty działające długoterminowo, które potrzebują świadomości w czasie rzeczywistym na temat aktualizacji konfiguracji. A walidacja zapewnia, że twoja aplikacja wygasa głośno, gdy brakuje wymaganych wartości.
Wnioski
[10:10 - 10:15] W skrócie: zarejestruj swoją konfigurację za pomocą AddOptions<t>().BindConfiguration("SectionName") w Program.cs, wstrzyknij jeden z trzech interfejsów (IOptions, IOptionsSnapshot lub IOptionsMonitor) w zależności od wymagań dotyczących przeładowania i dodaj .Validate(), aby wychwytywać brakujące wartości przy uruchomieniu, a nie podczas działania.
Wzorzec działa w każdym typie projektu .NET: Blazor, ASP.NET Core, aplikacje konsolowe, usługi pracownicze. Rejestracja jest taka sama wszędzie.
Przykład tip: Jeśli nie jesteś pewien, który interfejs użyć, zacznij od IOptions<t>. Jest najprostszy, nie ma żadnego obciążenia na żądanie i obejmuje zdecydowaną większość przypadków, w których konfiguracja jest ustawiona przy starcie i nie zmienia się. Przejdź do IOptionsSnapshot lub IOptionsMonitor tylko wtedy, gdy masz konkretną potrzebę dla przeładowania w czasie działania.
Obejrzyj pełne wideo na jego kanale YouTube i zyskaj więcej wglądu w wzorce konfiguracyjne C#.

