Delen via


Optiespatroon in .NET

Het optiespatroon maakt gebruik van klassen om sterk getypte toegang te bieden tot groepen gerelateerde instellingen. Wanneer configuratie-instellingen worden geïsoleerd door scenario's in afzonderlijke klassen, voldoet de app aan twee belangrijke principes voor software-engineering:

Opties bieden ook een mechanisme voor het valideren van configuratiegegevens. Zie de sectie Optiesvalidatie voor meer informatie.

Hiërarchische bindingsconfiguratie

De voorkeursmethode voor het lezen van gerelateerde configuratiewaarden is het gebruik van het optiespatroon. Het optiespatroon is mogelijk via de IOptions<TOptions> interface, waarbij de algemene typeparameter TOptions wordt beperkt tot een class. De IOptions<TOptions> kan later worden verstrekt via afhankelijkheidsinjectie. Zie Afhankelijkheidsinjectie in .NET voor meer informatie.

Als u bijvoorbeeld de gemarkeerde configuratiewaarden wilt lezen uit een appsettings.json-bestand :

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

Maak de volgende TransientFaultHandlingOptions klasse:

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

Wanneer u het optiespatroon gebruikt, wordt er een optiesklasse gebruikt:

  • Moet niet-abstract zijn met een openbare parameterloze constructor
  • Openbare eigenschappen voor lezen/schrijven bevatten om te binden (velden zijn niet gebonden)

De volgende code maakt deel uit van het bestand Program.cs C#en:

  • Roept ConfigurationBinder.Bind aan om de TransientFaultHandlingOptions klasse aan de "TransientFaultHandlingOptions" sectie te binden.
  • Geeft de configuratiegegevens weer.
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:

In de voorgaande code heeft het JSON-configuratiebestand de "TransientFaultHandlingOptions" sectie gebonden aan het TransientFaultHandlingOptions exemplaar. Hierdoor worden de eigenschappen van C#-objecten gehydrateerd met de bijbehorende waarden uit de configuratie.

ConfigurationBinder.Get<T> bindt en retourneert het opgegeven type. ConfigurationBinder.Get<T>kan handiger zijn dan het gebruik.ConfigurationBinder.Bind De volgende code laat zien hoe u deze kunt gebruiken ConfigurationBinder.Get<T> met de TransientFaultHandlingOptions klasse:

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

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

In de voorgaande code wordt het ConfigurationBinder.Get<T> gebruikt om een exemplaar van het TransientFaultHandlingOptions object te verkrijgen met de eigenschapswaarden die zijn gevuld met de onderliggende configuratie.

Belangrijk

De ConfigurationBinder klasse bevat verschillende API's, zoals .Bind(object instance) en .Get<T>() die niet zijn beperkt tot class. Wanneer u een van de optiesinterfaces gebruikt, moet u voldoen aan bovengenoemde beperkingen voor optiesklassen.

Een alternatieve benadering bij het gebruik van het optiespatroon is om de "TransientFaultHandlingOptions" sectie te binden en toe te voegen aan de container van de afhankelijkheidsinjectieservice. In de volgende code TransientFaultHandlingOptions wordt deze toegevoegd aan de servicecontainer met Configure en gebonden aan de configuratie:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

Het builder in het voorgaande voorbeeld is een instantie van HostApplicationBuilder.

Tip

De key parameter is de naam van de configuratiesectie die moet worden gezocht. Deze hoeft niet overeen te komen met de naam van het type dat het vertegenwoordigt. U kunt bijvoorbeeld een sectie met de naam "FaultHandling" hebben en deze kan worden vertegenwoordigd door de TransientFaultHandlingOptions klasse. In dit geval geeft u in plaats daarvan de GetSection functie door"FaultHandling". De nameof operator wordt gebruikt als gemak wanneer de benoemde sectie overeenkomt met het type waarmee deze overeenkomt.

Met behulp van de voorgaande code leest de volgende code de positieopties:

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

In de voorgaande code worden wijzigingen in het JSON-configuratiebestand nadat de app is gestart, niet gelezen. Als u wijzigingen wilt lezen nadat de app is gestart, gebruikt u IOptionsSnapshot of IOptionsMonitor om wijzigingen te controleren wanneer deze optreden en reageert u dienovereenkomstig.

Optiesinterfaces

IOptions<TOptions>:

  • Biedt geen ondersteuning voor:
    • Het lezen van configuratiegegevens nadat de app is gestart.
    • Benoemde opties
  • Is geregistreerd als een Singleton en kan worden geïnjecteerd in elke levensduur van de service.

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> is verantwoordelijk voor het maken van nieuwe opties instanties. Het heeft één Create methode. De standaard implementatie neemt alle geregistreerde IConfigureOptions<TOptions> en IPostConfigureOptions<TOptions> voert eerst alle configuraties uit, gevolgd door de postconfiguratie. Er wordt onderscheid gemaakt tussen IConfigureNamedOptions<TOptions> en IConfigureOptions<TOptions> wordt alleen de juiste interface aanroepen.

IOptionsMonitorCache<TOptions> wordt gebruikt voor IOptionsMonitor<TOptions> het opslaan van exemplaren in de cache TOptions . De IOptionsMonitorCache<TOptions> opties in de monitor worden ongeldig, zodat de waarde opnieuw wordt gecomputeerd (TryRemove). Waarden kunnen handmatig worden geïntroduceerd met TryAdd. De Clear methode wordt gebruikt wanneer alle benoemde exemplaren op aanvraag opnieuw moeten worden gemaakt.

IOptionsChangeTokenSource<TOptions> wordt gebruikt voor het ophalen van de IChangeToken wijzigingen in het onderliggende TOptions exemplaar. Zie Wijzigingsmeldingen voor meer informatie over wijzigingstokenprimitief.

Voordelen van optiesinterfaces

Als u een algemeen wrappertype gebruikt, kunt u de levensduur van de optie loskoppelen van de DI-container. De IOptions<TOptions>.Value interface biedt een abstractielaag, inclusief algemene beperkingen, voor uw optiestype. Dit biedt de volgende voordelen:

  • De evaluatie van het T configuratie-exemplaar wordt uitgesteld tot het openen van IOptions<TOptions>.Value, in plaats van wanneer het wordt geïnjecteerd. Dit is belangrijk omdat u de T optie op verschillende plaatsen kunt gebruiken en de semantiek van de levensduur kunt kiezen zonder dat u iets hoeft te Twijzigen.
  • Bij het registreren van opties van het type Thoeft u het T type niet expliciet te registreren. Dit is handig wanneer u een bibliotheek met eenvoudige standaardinstellingen ontwerpt en u niet wilt afdwingen dat de aanroeper opties registreert in de DI-container met een specifieke levensduur.
  • Vanuit het perspectief van de API is het mogelijk beperkingen voor het type T toe te staan (in dit geval T is dit beperkt tot een verwijzingstype).

IOptionsSnapshot gebruiken om bijgewerkte gegevens te lezen

Wanneer u gebruikt IOptionsSnapshot<TOptions>, worden opties eenmaal per aanvraag berekend wanneer deze worden geopend en in de cache worden opgeslagen voor de levensduur van de aanvraag. Wijzigingen in de configuratie worden gelezen nadat de app wordt gestart bij het gebruik van configuratieproviders die ondersteuning bieden voor het lezen van bijgewerkte configuratiewaarden.

Het verschil tussen IOptionsMonitor en IOptionsSnapshot is dat:

  • IOptionsMonitor is een singleton-service waarmee de huidige optiewaarden op elk gewenst moment worden opgehaald, wat met name handig is in singleton-afhankelijkheden.
  • IOptionsSnapshot is een scoped service en biedt een momentopname van de opties op het moment dat het IOptionsSnapshot<T> object wordt samengesteld. Momentopnamen van opties zijn ontworpen voor gebruik met tijdelijke en bereikafhankelijkheden.

In de volgende code wordt gebruikgemaakt van 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}");
    }
}

Met de volgende code wordt een configuratie-exemplaar geregistreerd waarmee TransientFaultHandlingOptions een binding wordt uitgevoerd:

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

In de voorgaande code wordt de Configure<TOptions> methode gebruikt om een configuratie-exemplaar te registreren waarmee TOptions een verbinding wordt uitgevoerd en worden de opties bijgewerkt wanneer de configuratie wordt gewijzigd.

IOptionsMonitor

Als u de optiesmonitor wilt gebruiken, worden optiesobjecten op dezelfde manier geconfigureerd vanuit een configuratiesectie.

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

In het volgende voorbeeld wordt gebruikgemaakt van 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}");
    }
}

In de voorgaande code worden wijzigingen in het JSON-configuratiebestand gewijzigd nadat de app is gestart.

Tip

Sommige bestandssystemen, zoals Docker-containers en netwerkshares, verzenden mogelijk niet betrouwbaar wijzigingsmeldingen. Wanneer u de IOptionsMonitor<TOptions> interface in deze omgevingen gebruikt, stelt u de DOTNET_USE_POLLING_FILE_WATCHER omgevingsvariabele 1 in op of true pollt u het bestandssysteem op wijzigingen. Het interval waarmee wijzigingen worden gecontroleerd, is elke vier seconden en kan niet worden geconfigureerd.

Zie Een .NET-app containeriseren voor meer informatie over Docker-containers.

Ondersteuning voor benoemde opties met IConfigureNamedOptions

Benoemde opties:

  • Dit is handig wanneer meerdere configuratiesecties verbinding maken met dezelfde eigenschappen.
  • Zijn hoofdlettergevoelig.

Houd rekening met het volgende appsettings.json-bestand :

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

In plaats van twee klassen te maken om te binden Features:Personalize en Features:WeatherStationwordt de volgende klasse gebruikt voor elke sectie:

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

Met de volgende code worden de benoemde opties geconfigureerd:

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

Met de volgende code worden de benoemde opties weergegeven:

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 opties zijn benoemde exemplaren. IConfigureOptions<TOptions>exemplaren worden behandeld als gericht op het Options.DefaultName exemplaar.string.Empty IConfigureNamedOptions<TOptions> implementeert IConfigureOptions<TOptions>ook . De standaard implementatie van de IOptionsFactory<TOptions> bevat logica om elk op de juiste manier te gebruiken. De null benoemde optie wordt gebruikt om alle benoemde exemplaren te targeten in plaats van op een specifiek benoemd exemplaar. ConfigureAll en PostConfigureAll gebruik deze conventie.

OptionsBuilder-API

OptionsBuilder<TOptions> wordt gebruikt voor het configureren van TOptions exemplaren. OptionsBuilder stroomlijnt het maken van benoemde opties, omdat het slechts één parameter is voor de eerste AddOptions<TOptions>(string optionsName) aanroep in plaats van in alle volgende aanroepen weer te geven. Optiesvalidatie en de ConfigureOptions overbelastingen die serviceafhankelijkheden accepteren, zijn alleen beschikbaar via OptionsBuilder.

OptionsBuilder wordt gebruikt in de sectie Optiesvalidatie .

DI-services gebruiken om opties te configureren

Services kunnen worden geopend via afhankelijkheidsinjectie tijdens het configureren van opties op twee manieren:

  • Geef een configuratiedelegatie door om te configureren op OptionsBuilder<TOptions>. OptionsBuilder<TOptions> biedt overbelastingen van Configureren waarmee maximaal vijf services kunnen worden gebruikt om opties te configureren:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Maak een type dat het type als een service implementeert IConfigureOptions<TOptions> of IConfigureNamedOptions<TOptions> registreert.

U wordt aangeraden een configuratiedelegend door te geven aan Configureren, omdat het maken van een service complexer is. Het maken van een type is gelijk aan wat het framework doet bij het aanroepen van Configureren. Met Aanroepen configureren registreert een tijdelijke algemene IConfigureNamedOptions<TOptions>, die een constructor heeft die de algemene servicetypen accepteert die zijn opgegeven.

Validatie van opties

Met optiesvalidatie kunnen optiewaarden worden gevalideerd.

Houd rekening met het volgende appsettings.json-bestand :

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

De volgende klasse bindt aan de "MyCustomSettingsSection" configuratiesectie en past een aantal DataAnnotations regels toe:

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 de voorgaande SettingsOptions klasse bevat de ConfigurationSectionName eigenschap de naam van de configuratiesectie waaraan moet worden gekoppeld. In dit scenario bevat het optiesobject de naam van de configuratiesectie.

Tip

De naam van de configuratiesectie is onafhankelijk van het configuratieobject waaraan het is gebonden. Met andere woorden, een configuratiesectie met de naam "FooBarOptions" kan worden gebonden aan een optiesobject met de naam ZedOptions. Hoewel het misschien gebruikelijk is om ze dezelfde naam te geven, is het niet nodig en kan het naamconflicten daadwerkelijk veroorzaken.

De volgende code:

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

De ValidateDataAnnotations extensiemethode wordt gedefinieerd in het NuGet-pakket Microsoft.Extensions.Options.DataAnnotations .

De volgende code geeft de configuratiewaarden weer of rapporteert validatiefouten:

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

Met de volgende code wordt een complexere validatieregel toegepast met behulp van een gemachtigde:

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

De validatie vindt plaats tijdens runtime, maar u kunt deze zo configureren dat deze wordt uitgevoerd bij het opstarten door in plaats daarvan een aanroep te koppelen aan 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();

Vanaf .NET 8 kunt u een alternatieve API gebruiken, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String)waarmee validatie wordt ingeschakeld voor een specifiek type opties:

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 voor complexe validatie

De volgende klasse implementeert IValidateOptions<TOptions>:

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 schakelt het verplaatsen van de validatiecode naar een klasse in.

Notitie

Deze voorbeeldcode is afhankelijk van het NuGet-pakket Microsoft.Extensions.Configuration.Json .

Met behulp van de voorgaande code wordt validatie ingeschakeld bij het configureren van services met de volgende code:

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

Opties na configuratie

Stel na de configuratie in met IPostConfigureOptions<TOptions>. Na de configuratie wordt uitgevoerd nadat alle IConfigureOptions<TOptions> configuraties zijn uitgevoerd en kan dit handig zijn in scenario's waarin u de configuratie moet overschrijven:

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

PostConfigure is beschikbaar voor het na configureren van benoemde opties:

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

Gebruik PostConfigureAll dit om alle configuratie-exemplaren na de configuratie te configureren:

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

Zie ook