Share via


Alternativmönster i .NET

Alternativmönstret använder klasser för att ge starkt typbaserad åtkomst till grupper med relaterade inställningar. När konfigurationsinställningarna isoleras efter scenario i separata klasser följer appen två viktiga principer för programvaruteknik:

Alternativen tillhandahåller också en mekanism för att verifiera konfigurationsdata. Mer information finns i avsnittet Alternativvalidering .

Bind hierarkisk konfiguration

Det bästa sättet att läsa relaterade konfigurationsvärden är att använda alternativmönstret. Alternativmönstret är möjligt via IOptions<TOptions> gränssnittet, där den generiska typparametern TOptions är begränsad till en class. IOptions<TOptions> Kan senare tillhandahållas via beroendeinmatning. Mer information finns i Beroendeinmatning i .NET.

Om du till exempel vill läsa de markerade konfigurationsvärdena från en appsettings.json fil:

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

Skapa följande TransientFaultHandlingOptions klass:

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

När du använder alternativmönstret, en alternativklass:

  • Måste vara icke-abstrakt med en offentlig parameterlös konstruktor
  • Innehåller offentliga skrivskyddade egenskaper för bindning (fält är inte bundna)

Följande kod är en del av filen Program.cs C# och:

  • Anropar ConfigurationBinder.Bind för att binda TransientFaultHandlingOptions klassen till avsnittet "TransientFaultHandlingOptions" .
  • Visar konfigurationsdata.
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:

I föregående kod har JSON-konfigurationsfilen sitt "TransientFaultHandlingOptions" avsnitt bundet till instansen TransientFaultHandlingOptions . Detta återfuktar egenskaperna för C#-objekt med motsvarande värden från konfigurationen.

ConfigurationBinder.Get<T> binder och returnerar den angivna typen. ConfigurationBinder.Get<T> kan vara bekvämare än att använda ConfigurationBinder.Bind. Följande kod visar hur du använder ConfigurationBinder.Get<T> med TransientFaultHandlingOptions klassen:

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

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

I föregående kod ConfigurationBinder.Get<T> används för att hämta en instans av TransientFaultHandlingOptions objektet med dess egenskapsvärden ifyllda från den underliggande konfigurationen.

Viktigt!

Klassen ConfigurationBinder exponerar flera API:er, till exempel .Bind(object instance) och .Get<T>() som inte är begränsade till class. När du använder något av alternativens gränssnitt måste du följa ovan nämnda alternativklassbegränsningar.

En alternativ metod när du använder alternativmönstret är att binda "TransientFaultHandlingOptions" avsnittet och lägga till det i containern för beroendeinmatningstjänsten. I följande kod TransientFaultHandlingOptions läggs till i tjänstcontainern med Configure och är bunden till konfigurationen:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

I builder föregående exempel finns en instans av HostApplicationBuilder.

Dricks

Parametern key är namnet på konfigurationsavsnittet som du vill söka efter. Den behöver inte matcha namnet på den typ som representerar den. Du kan till exempel ha ett avsnitt med namnet "FaultHandling" och det kan representeras av TransientFaultHandlingOptions klassen. I det här fallet skulle du skicka "FaultHandling" till GetSection funktionen i stället. Operatorn nameof används som en bekvämlighet när det namngivna avsnittet matchar den typ som den motsvarar.

Med hjälp av föregående kod läser följande kod positionsalternativen:

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

I föregående kod läss inte ändringar i JSON-konfigurationsfilen när appen har startats. Om du vill läsa ändringar när appen har startat använder du IOptionsSnapshot eller IOptionsMonitor för att övervaka ändringar när de inträffar och reagera därefter.

Alternativgränssnitt

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> ansvarar för att skapa nya alternativinstanser. Den har en enda Create metod. Standardimplementeringen tar alla registrerade IConfigureOptions<TOptions> och IPostConfigureOptions<TOptions> kör alla konfigurationer först, följt av efterkonfigurationen. Den skiljer mellan IConfigureNamedOptions<TOptions> och IConfigureOptions<TOptions> och anropar bara rätt gränssnitt.

IOptionsMonitorCache<TOptions> används av IOptionsMonitor<TOptions> för att cachelagrar TOptions instanser. Ogiltigförklarar IOptionsMonitorCache<TOptions> alternativinstanser i övervakaren så att värdet omberäknas (TryRemove). Värden kan introduceras manuellt med TryAdd. Metoden Clear används när alla namngivna instanser ska återskapas på begäran.

IOptionsChangeTokenSource<TOptions> används för att hämta som IChangeToken spårar ändringar i den underliggande TOptions instansen. Mer information om primitiver för ändringstoken finns i Ändra meddelanden.

Fördelar med alternativgränssnitt

Om du använder en allmän omslutningstyp kan du frikoppla livslängden för alternativet från DI-containern. Gränssnittet IOptions<TOptions>.Value innehåller ett abstraktionslager, inklusive allmänna begränsningar, för din alternativtyp. Detta ger följande fördelar:

  • Utvärderingen av konfigurationsinstansen T skjuts upp till åtkomsten av IOptions<TOptions>.Value, i stället för när den matas in. Detta är viktigt eftersom du kan använda T alternativet från olika platser och välja livslängdssemantik utan att ändra något om T.
  • När du registrerar alternativ av typen Tbehöver du inte uttryckligen T registrera typen. Det här är en bekvämlighet när du redigerar ett bibliotek med enkla standardvärden och du inte vill tvinga anroparen att registrera alternativ i DI-containern med en viss livslängd.
  • Från API:ets perspektiv tillåter det begränsningar för typen T (i det här fallet T begränsas den till en referenstyp).

Använda IOptionsSnapshot för att läsa uppdaterade data

När du använder IOptionsSnapshot<TOptions>beräknas alternativen en gång per begäran vid åtkomst och cachelagras under begärans livslängd. Ändringar i konfigurationen läse när appen startar när du använder konfigurationsproviders som stöder läsning av uppdaterade konfigurationsvärden.

Skillnaden mellan IOptionsMonitor och IOptionsSnapshot är att:

  • IOptionsMonitor är en singleton-tjänst som hämtar aktuella alternativvärden när som helst, vilket är särskilt användbart i singleton-beroenden.
  • IOptionsSnapshot är en begränsad tjänst och ger en ögonblicksbild av alternativen när IOptionsSnapshot<T> objektet skapas. Ögonblicksbilder av alternativ är utformade för användning med tillfälliga och begränsade beroenden.

Följande kod använder 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}");
    }
}

Följande kod registrerar en konfigurationsinstans som TransientFaultHandlingOptions binder mot:

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

I föregående kod Configure<TOptions> används metoden för att registrera en konfigurationsinstans som TOptions ska bindas mot och uppdaterar alternativen när konfigurationen ändras.

IOptionsMonitor

Om du vill använda alternativövervakaren konfigureras alternativobjekt på samma sätt från ett konfigurationsavsnitt.

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

I följande exempel används 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}");
    }
}

I föregående kod läss ändringar i JSON-konfigurationsfilen när appen har startats.

Dricks

Vissa filsystem, till exempel Docker-containrar och nätverksresurser, kanske inte skickar ändringsmeddelanden på ett tillförlitligt sätt. När du använder IOptionsMonitor<TOptions> gränssnittet i dessa miljöer anger du DOTNET_USE_POLLING_FILE_WATCHER miljövariabeln till 1 eller true för att söka efter ändringar i filsystemet. Det intervall med vilket ändringar avsöks är var fjärde sekund och kan inte konfigureras.

Mer information om Docker-containrar finns i Containerisera en .NET-app.

Namngivna alternativ stöder användning av IConfigureNamedOptions

Namngivna alternativ:

  • Är användbara när flera konfigurationsavsnitt binder till samma egenskaper.
  • Är skiftlägeskänsliga.

Överväg följande appsettings.json fil:

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

I stället för att skapa två klasser för bindning Features:Personalize och Features:WeatherStationanvänds följande klass för varje avsnitt:

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

Följande kod konfigurerar de namngivna alternativen:

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

Följande kod visar de namngivna alternativen:

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

Alla alternativ heter instanser. IConfigureOptions<TOptions> instanser behandlas som mål för instansen Options.DefaultName , som är string.Empty. IConfigureNamedOptions<TOptions> implementerar IConfigureOptions<TOptions>också . Standardimplementeringen av IOptionsFactory<TOptions> har logik för att använda var och en på rätt sätt. Det null namngivna alternativet används för att rikta alla namngivna instanser i stället för en specifik namngiven instans. ConfigureAll och PostConfigureAll använd den här konventionen.

OptionsBuilder API

OptionsBuilder<TOptions> används för att konfigurera TOptions instanser. OptionsBuilder effektiviserar skapandet av namngivna alternativ eftersom det bara är en enda parameter till det första AddOptions<TOptions>(string optionsName) anropet i stället för att visas i alla efterföljande anrop. Alternativvalidering och överlagringar ConfigureOptions som accepterar tjänstberoenden är endast tillgängliga via OptionsBuilder.

OptionsBuilder används i avsnittet Alternativvalidering .

Använda DI-tjänster för att konfigurera alternativ

Tjänster kan nås från beroendeinmatning när du konfigurerar alternativ på två sätt:

  • Skicka ett konfigurationsdelegat till Konfigurera på AlternativBuilder<TOptions>. OptionsBuilder<TOptions> innehåller överlagringar av Konfigurera som tillåter användning av upp till fem tjänster för att konfigurera alternativ:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Skapa en typ som implementerar IConfigureOptions<TOptions> eller IConfigureNamedOptions<TOptions> registrerar typen som en tjänst.

Vi rekommenderar att du skickar en konfigurationsdelegat till Konfigurera eftersom det är mer komplext att skapa en tjänst. Att skapa en typ motsvarar vad ramverket gör när du anropar Konfigurera. Anropa Konfigurera registrerar en tillfällig allmän IConfigureNamedOptions<TOptions>, som har en konstruktor som accepterar de allmänna tjänsttyper som anges.

Alternativvalidering

Alternativvalidering gör att alternativvärden kan verifieras.

Överväg följande appsettings.json fil:

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

Följande klass binder till konfigurationsavsnittet "MyCustomSettingsSection" och tillämpar ett par DataAnnotations regler:

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

I föregående SettingsOptions klass ConfigurationSectionName innehåller egenskapen namnet på konfigurationsavsnittet som ska bindas till. I det här scenariot innehåller alternativobjektet namnet på dess konfigurationsavsnitt.

Dricks

Namnet på konfigurationsavsnittet är oberoende av konfigurationsobjektet som det är bindning till. Med andra ord kan ett konfigurationsavsnitt med namnet "FooBarOptions" bindas till ett alternativobjekt med namnet ZedOptions. Även om det kan vara vanligt att namnge dem på samma sätt, är det inte nödvändigt och kan faktiskt orsaka namnkonflikter.

Följande kod:

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

Tilläggsmetoden ValidateDataAnnotations definieras i NuGet-paketet Microsoft.Extensions.Options.DataAnnotations .

Följande kod visar konfigurationsvärdena eller rapporterar valideringsfel:

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

Följande kod tillämpar en mer komplex verifieringsregel med hjälp av ett ombud:

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

Verifieringen sker vid körning, men du kan konfigurera den så att den sker vid start genom att i stället länka ett anrop till 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();

Från och med .NET 8 kan du använda ett alternativt API, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), som aktiverar validering vid start för en specifik alternativtyp:

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 för komplex validering

Följande klass implementerar 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 gör det möjligt att flytta valideringskoden till en klass.

Kommentar

Den här exempelkoden förlitar sig på NuGet-paketet Microsoft.Extensions.Configuration.Json .

Med hjälp av föregående kod aktiveras verifiering när tjänster konfigureras med följande kod:

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

Alternativ efter konfiguration

Ange efterkonfiguration med IPostConfigureOptions<TOptions>. Efter konfigurationen körs när all IConfigureOptions<TOptions> konfiguration har inträffat och kan vara användbar i scenarier när du behöver åsidosätta konfigurationen:

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

PostConfigure är tillgängligt för efterkonfigurering av namngivna alternativ:

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

Använd PostConfigureAll för att efterkonfigurera alla konfigurationsinstanser:

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

Se även