Wzorzec opcji na platformie .NET

Wzorzec opcji używa klas w celu zapewnienia silnie typizowanego dostępu do grup powiązanych ustawień. Gdy ustawienia konfiguracji są izolowane według scenariusza w oddzielnych klasach, aplikacja jest zgodna z dwoma ważnymi zasadami inżynierii oprogramowania:

Opcje udostępniają również mechanizm sprawdzania poprawności danych konfiguracji. Aby uzyskać więcej informacji, zobacz sekcję Walidacja opcji.

Konfiguracja hierarchiczna powiązania

Preferowanym sposobem odczytywania powiązanych wartości konfiguracji jest użycie wzorca opcji. Wzorzec opcji jest możliwy za pośrednictwem interfejsu IOptions<TOptions> , gdzie parametr TOptions typu ogólnego jest ograniczony do classklasy . Można IOptions<TOptions> później przekazać za pomocą wstrzykiwania zależności. Aby uzyskać więcej informacji, zobacz Wstrzykiwanie zależności na platformie .NET.

Aby na przykład odczytać wyróżnione wartości konfiguracji z pliku appsettings.json :

{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

Utwórz następującą klasę TransientFaultHandlingOptions:

public sealed class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}

W przypadku korzystania ze wzorca opcji klasa opcji:

  • Musi być nie abstrakcyjny z publicznym konstruktorem bez parametrów
  • Zawierać właściwości publicznego odczytu i zapisu do powiązania (pola niepowiązane)

Poniższy kod jest częścią pliku Program.cs C#i:

  • Wywołanie elementu ConfigurationBinder.Bind w celu powiązania klasy TransientFaultHandlingOptions z sekcją "TransientFaultHandlingOptions".
  • Wyświetlenie danych konfiguracji .
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:

W poprzednim kodzie plik konfiguracji JSON ma jego "TransientFaultHandlingOptions" sekcję powiązaną z wystąpieniem TransientFaultHandlingOptions . Spowoduje to nawodnienie właściwości obiektów języka C# odpowiednimi wartościami z konfiguracji.

Element ConfigurationBinder.Get<T> tworzy powiązanie i zwraca określony typ. Element ConfigurationBinder.Get<T> może być wygodniejszy w użyciu niż element ConfigurationBinder.Bind. W poniższym kodzie pokazano sposób użycia elementu ConfigurationBinder.Get<T> z klasą TransientFaultHandlingOptions:

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

W poprzednim kodzie ConfigurationBinder.Get<T> obiekt jest używany do uzyskiwania wystąpienia TransientFaultHandlingOptions obiektu z jego wartościami właściwości wypełnionymi z podstawowej konfiguracji.

Ważne

Klasa ConfigurationBinder uwidacznia kilka interfejsów API, takich jak .Bind(object instance) i .Get<T>() które nieograniczone do class. W przypadku korzystania z dowolnego interfejsu Opcje należy przestrzegać wyżej wymienionych ograniczeń klasy opcji.

Alternatywną metodą użycia wzorca opcji jest powiązanie "TransientFaultHandlingOptions" sekcji i dodanie jej do kontenera usługi wstrzykiwania zależności. W poniższym kodzie element TransientFaultHandlingOptions jest dodawany do kontenera usługi przy użyciu Configure, a następnie jest tworzone jego powiązanie z konfiguracją:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

W builder poprzednim przykładzie jest wystąpieniem HostApplicationBuilderklasy .

Napiwek

Parametr key jest nazwą sekcji konfiguracji do wyszukania. Nie musi być zgodna z nazwą typu, który go reprezentuje. Na przykład możesz mieć sekcję o nazwie "FaultHandling" i może być reprezentowana przez klasę TransientFaultHandlingOptions . W tym przypadku należy przekazać "FaultHandling" do GetSection funkcji . Operator nameof jest używany jako wygoda, gdy nazwana sekcja jest zgodna z typem, do którego odpowiada.

Przy użyciu poprzedniego kodu następujący kod odczytuje opcje położenia:

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}");
    }
}

W poprzednim kodzie zmiany w pliku konfiguracji JSON po uruchomieniu aplikacji nieodczytywane. Aby odczytać zmiany po uruchomieniu aplikacji, użyj funkcji IOptionsSnapshot lub IOptionsMonitor , aby monitorować zmiany w miarę ich występowania i odpowiednio reagować.

Interfejsy opcji

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> jest odpowiedzialny za tworzenie nowych wystąpień opcji. Ma jedną Create metodę. Domyślna implementacja pobiera wszystkie zarejestrowane IConfigureOptions<TOptions> i IPostConfigureOptions<TOptions> uruchamia najpierw wszystkie konfiguracje, a następnie po konfiguracji. Rozróżnia między elementami IConfigureNamedOptions<TOptions> i IConfigureOptions<TOptions> i wywołuje tylko odpowiedni interfejs.

IOptionsMonitorCache<TOptions> jest używany przez IOptionsMonitor<TOptions> program do buforowania TOptions wystąpień. Unieważnia IOptionsMonitorCache<TOptions> wystąpienia opcji w monitorze, aby wartość została ponownie skompilowana (TryRemove). Wartości można wprowadzić ręcznie za pomocą TryAddpolecenia . Metoda Clear jest używana, gdy wszystkie nazwane wystąpienia powinny być ponownie tworzone na żądanie.

IOptionsChangeTokenSource<TOptions> Służy do pobierania danych IChangeToken , które śledzą zmiany w wystąpieniu bazowym TOptions . Aby uzyskać więcej informacji na temat elementów pierwotnych tokenu zmiany, zobacz Zmienianie powiadomień.

Korzyści z interfejsów opcji

Użycie ogólnego typu otoki umożliwia oddzielenie okresu istnienia opcji z kontenera DI. Interfejs IOptions<TOptions>.Value zapewnia warstwę abstrakcji, w tym ogólne ograniczenia, dla typu opcji. Oferuje to następujące korzyści:

  • Ocena T wystąpienia konfiguracji jest odroczona do uzyskiwania IOptions<TOptions>.Valuedostępu do elementu , a nie po wstrzyknięciu. Jest to ważne, ponieważ można korzystać T z opcji z różnych miejsc i wybrać semantyka okresu istnienia bez zmiany niczego o T.
  • Podczas rejestrowania opcji typu Tnie trzeba jawnie rejestrować T typu. Jest to wygoda, gdy tworzysz bibliotekę z prostymi wartościami domyślnymi i nie chcesz wymuszać, aby obiekt wywołujący zarejestrował opcje w kontenerze DI z określonym okresem istnienia.
  • Z perspektywy interfejsu API umożliwia ograniczenie typu T (w tym przypadku T jest ograniczone do typu odwołania).

Odczytywanie zaktualizowanych danych przy użyciu funkcji IOptionsSnapshot

W przypadku używania IOptionsSnapshot<TOptions>opcji opcje są obliczane raz na żądanie w przypadku uzyskania dostępu i są buforowane przez okres istnienia żądania. Zmiany konfiguracji są odczytywane po uruchomieniu aplikacji podczas korzystania z dostawców konfiguracji obsługujących odczytywanie zaktualizowanych wartości konfiguracji.

Różnica między elementami IOptionsMonitor i IOptionsSnapshot polega na tym, że:

  • IOptionsMonitor to pojedyncza usługa , która w dowolnym momencie pobiera bieżące wartości opcji, co jest szczególnie przydatne w zależnościach pojedynczych.
  • IOptionsSnapshotjest usługą o określonym zakresie i udostępnia migawkę opcji w momencie IOptionsSnapshot<T> konstruowania obiektu. Migawki opcji są przeznaczone do użytku z zależnościami przejściowymi i o określonym zakresie.

Poniższy kod używa metody IOptionsSnapshot<TOptions>.

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

Poniższy kod rejestruje wystąpienie konfiguracji, które TransientFaultHandlingOptions wiąże się z:

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

W poprzednim kodzie Configure<TOptions> metoda jest używana do rejestrowania wystąpienia konfiguracji, które TOptions będzie powiązane, i aktualizuje opcje po zmianie konfiguracji.

IOptionsMonitor

Aby użyć monitora opcji, obiekty opcji są konfigurowane w taki sam sposób w sekcji konfiguracji.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

W poniższym przykładzie użyto metody IOptionsMonitor<TOptions>:

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}");
    }
}

W poprzednim kodzie zmiany w pliku konfiguracji JSON po uruchomieniu aplikacji są odczytywane.

Napiwek

Niektóre systemy plików, takie jak kontenery platformy Docker i udziały sieciowe, mogą nie niezawodnie wysyłać powiadomień o zmianie. W przypadku korzystania z interfejsu IOptionsMonitor<TOptions> w tych środowiskach ustaw zmienną DOTNET_USE_POLLING_FILE_WATCHER środowiskową na 1 lub true sonduj system plików pod kątem zmian. Interwał sondowania zmian jest co cztery sekundy i nie można go skonfigurować.

Aby uzyskać więcej informacji na temat kontenerów platformy Docker, zobacz Containerize a .NET app (Konteneryzowanie aplikacji .NET).

Obsługa opcji nazwanych przy użyciu funkcji IConfigureNamedOptions

Nazwane opcje:

  • Są przydatne, gdy wiele sekcji konfiguracji wiąże się z tymi samymi właściwościami.
  • Uwzględnia wielkość liter.

Rozważ następujący plik appsettings.json :

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

Zamiast tworzyć dwie klasy do powiązania Features:Personalize i Features:WeatherStation, dla każdej sekcji jest używana następująca klasa:

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; }
}

Poniższy kod konfiguruje nazwane opcje:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

Poniższy kod wyświetla nazwane opcje:

public class sealed Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

Wszystkie opcje są nazwane wystąpienia. IConfigureOptions<TOptions> wystąpienia są traktowane jako docelowe Options.DefaultName wystąpienie, czyli string.Empty. IConfigureNamedOptions<TOptions> implementuje IConfigureOptions<TOptions>również funkcję . Domyślna implementacja elementu IOptionsFactory<TOptions> ma logikę do użycia odpowiednio. Nazwana null opcja służy do określania wartości docelowej wszystkich nazwanych wystąpień zamiast określonego nazwanego wystąpienia. ConfigureAll i PostConfigureAll stosować tę konwencję.

OptionsBuilder API

OptionsBuilder<TOptions> służy do konfigurowania TOptions wystąpień. OptionsBuilder Usprawnia tworzenie nazwanych opcji, ponieważ jest to tylko jeden parametr do początkowego AddOptions<TOptions>(string optionsName) wywołania zamiast pojawiać się we wszystkich kolejnych wywołaniach. Opcje weryfikacji i ConfigureOptions przeciążenia akceptujące zależności usługi są dostępne tylko za pośrednictwem .OptionsBuilder

OptionsBuilderjest używany w sekcji Walidacja opcji.

Konfigurowanie opcji przy użyciu usług DI

Dostęp do usług można uzyskać z iniekcji zależności podczas konfigurowania opcji na dwa sposoby:

  • Przekaż delegata konfiguracji, aby skonfigurować w obszarze OptionsBuilder<TOptions>. OptionsBuilder<TOptions> Udostępnia przeciążenia konfiguracji , które umożliwiają korzystanie z maksymalnie pięciu usług do konfigurowania opcji:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Utwórz typ implementujący IConfigureOptions<TOptions> lub IConfigureNamedOptions<TOptions> rejestrujący typ jako usługę.

Zalecamy przekazanie delegata konfiguracji do konfiguracji, ponieważ tworzenie usługi jest bardziej złożone. Tworzenie typu jest równoważne z tym, co platforma wykonuje podczas wywoływania konfiguracji. Wywoływanie polecenia Configure powoduje zarejestrowanie przejściowego ogólnego IConfigureNamedOptions<TOptions>typu , który ma konstruktor, który akceptuje określone typy usług ogólnych.

Walidacja opcji

Walidacja opcji umożliwia zweryfikowanie wartości opcji.

Rozważ następujący plik appsettings.json :

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

Następująca klasa wiąże się z sekcją "MyCustomSettingsSection" konfiguracji i stosuje kilka DataAnnotations reguł:

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; }
}

W poprzedniej SettingsOptions klasie ConfigurationSectionName właściwość zawiera nazwę sekcji konfiguracji, z która ma być powiązana. W tym scenariuszu obiekt options zawiera nazwę sekcji konfiguracji.

Napiwek

Nazwa sekcji konfiguracji jest niezależna od obiektu konfiguracji, z którą jest powiązanie. Innymi słowy, sekcja konfiguracji o nazwie "FooBarOptions" może być powiązana z obiektem opcji o nazwie ZedOptions. Chociaż nazwa może być taka sama, nie jest to konieczne i może powodować konflikty nazw.

Następujący kod powoduje:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

Metoda rozszerzenia jest definiowana ValidateDataAnnotations w pakiecie NuGet Microsoft.Extensions.Options.DataAnnotations .

Poniższy kod wyświetla wartości konfiguracji lub zgłasza błędy walidacji:

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);
            }
        }
    }
}

Poniższy kod stosuje bardziej złożoną regułę walidacji przy użyciu delegata:

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.");

Walidacja odbywa się w czasie wykonywania, ale można ją skonfigurować tak, aby wystąpiła podczas uruchamiania, zamiast tego łącząc wywołanie z poleceniem ValidateOnStart:

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();

Począwszy od platformy .NET 8, można użyć alternatywnego interfejsu API, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String)który umożliwia walidację podczas uruchamiania dla określonego typu opcji:

builder.Services
    .AddOptionsWithValidateOnStart<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.");

IValidateOptions w przypadku złożonej weryfikacji

Następująca klasa implementuje IValidateOptions<TOptions>element :

using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

sealed partial class ValidateSettingsOptions(
    IConfiguration config)
    : IValidateOptions<SettingsOptions>
{
    public SettingsOptions? Settings { get; private set; } =
        config.GetSection(SettingsOptions.ConfigurationSectionName)
              .Get<SettingsOptions>();

    public ValidateOptionsResult Validate(string? name, SettingsOptions options)
    {
        StringBuilder? failure = null;
    
        if (!ValidationRegex().IsMatch(options.SiteTitle))
        {
            (failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
        }

        if (options.Scale is < 0 or > 1_000)
        {
            (failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
        }

        if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
        {
            (failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
        }

        return failure is not null
            ? ValidateOptionsResult.Fail(failure.ToString())
            : ValidateOptionsResult.Success;
    }

    [GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
    private static partial Regex ValidationRegex();
}

IValidateOptions umożliwia przeniesienie kodu weryfikacji do klasy.

Uwaga

Ten przykładowy kod opiera się na pakiecie NuGet Microsoft.Extensions.Configuration.Json .

Korzystając z powyższego kodu, walidacja jest włączona podczas konfigurowania usług przy użyciu następującego kodu:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

Opcje po konfiguracji

Ustaw konfigurację po konfiguracji za pomocą polecenia IPostConfigureOptions<TOptions>. Po zakończeniu konfiguracji wszystkie konfiguracje IConfigureOptions<TOptions> mogą być przydatne w scenariuszach, w których trzeba zastąpić konfigurację:

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

PostConfigure program jest dostępny do po skonfigurowaniu nazwanych opcji:

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Użyj PostConfigureAll polecenia , aby po skonfigurowaniu wszystkich wystąpień konfiguracji:

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Zobacz też