Modello options in .NET

Il modello options usa le classi per fornire l'accesso fortemente tipizzato a gruppi di impostazioni correlate. Quando le impostazioni di configurazione vengono isolate in base allo scenario in classi separate, l'app aderisce a due importanti principi di progettazione del software:

Le opzioni offrono anche un meccanismo per convalidare i dati di configurazione. Per altre informazioni, vedere la sezione Opzioni di convalida.

Associare la configurazione gerarchica

Il modo preferito per leggere i valori di configurazione correlati prevede l'uso del modello di opzioni. Il modello options è possibile tramite l'interfaccia IOptions<TOptions>, in cui il parametro di tipo generico TOptions è vincolato a un oggetto class. L'interfaccia IOptions<TOptions> può essere specificata in un secondo momento tramite l'inserimento delle dipendenze. Per altre informazioni, vedere Inserimento delle dipendenze in .NET.

Ad esempio, per leggere i valori di configurazione evidenziati da un file appsettings.json:

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

Creare la classe TransientFaultHandlingOptions seguente:

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

Quando si usa il modello options, una classe options:

  • Deve essere non astratta con un costruttore pubblico senza parametri
  • Deve contenere proprietà pubbliche di lettura/scrittura da associare (i campi sono non associati)

Il codice seguente fa parte del file C# Program.cs e:

  • Chiama configurationBinder.Bind per associare la classe TransientFaultHandlingOptions alla sezione "TransientFaultHandlingOptions".
  • Visualizza i dati di configurazione di .
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:

Nel codice precedente la sezione "TransientFaultHandlingOptions" del file di configurazione JSON è associata all'istanza di TransientFaultHandlingOptions. In questo modo, le proprietà degli oggetti C# vengono attivate con i valori corrispondenti della configurazione.

ConfigurationBinder.Get<T> associa e restituisce il tipo specificato. ConfigurationBinder.Get<T> può risultare più utile rispetto all'uso di ConfigurationBinder.Bind. Il codice seguente mostra come usare ConfigurationBinder.Get<T> con la classe TransientFaultHandlingOptions:

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

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

Nel codice precedente, si usa ConfigurationBinder.Get<T> per acquisire un'istanza dell'oggetto TransientFaultHandlingOptions con i relativi valori delle proprietà popolati dalla configurazione sottostante.

Importante

La classe ConfigurationBinder espone diverse API, ad esempio .Bind(object instance) e .Get<T>() che sono non vincolate a class. Quando si usa una qualsiasi delle interfacce Options, è necessario rispettare i vincoli della classe options indicati in precedenza.

Un approccio alternativo quando si usa il modello options consiste nell'associare la sezione "TransientFaultHandlingOptions" e aggiungerla al contenitore del servizio di inserimento di dipendenze. Nel codice seguente TransientFaultHandlingOptions viene aggiunta al contenitore del servizio con Configure e associata alla configurazione:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

builder nell'esempio precedente è un'istanza di HostApplicationBuilder.

Suggerimento

Il parametro key è il nome della sezione di configurazione da cercare. Non deve corrispondere al nome del tipo che la rappresenta. Ad esempio, potrebbe essere presente una sezione denominata "FaultHandling" e rappresentarla dalla classe TransientFaultHandlingOptions. In questo caso, verrebbe passato "FaultHandling" alla funzione GetSection. L'operatore nameof viene usato per praticità quando la sezione denominata corrisponde al tipo previsto.

Quando si usa il codice precedente, il codice seguente legge le opzioni di posizione:

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

Nel codice precedente le modifiche al file di configurazione JSON dopo l'avvio dell'app non vengono lette. Per leggere le modifiche dopo l'avvio dell'app, usare IOptionsSnapshot o IOptionsMonitor per monitorare le modifiche man mano che si verificano e reagire di conseguenza.

Interfacce per le opzioni

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> è responsabile della creazione di nuove istanze di opzioni. Include un singolo metodo Create. L'implementazione predefinita accetta tutte le interfacce IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> registrate ed esegue tutte le configurazioni, seguite dalla post-configurazione. Fa distinzione tra IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> e chiama solo l'interfaccia appropriata.

IOptionsMonitorCache<TOptions> viene usata da IOptionsMonitor<TOptions> per memorizzare nella cache le istanze di TOptions. IOptionsMonitorCache<TOptions> invalida le istanze delle opzioni nel monitoraggio in modo che il valore venga ricalcolato (TryRemove). I valori possono essere introdotti manualmente con TryAdd. Il metodo Clear viene usato quando tutte le istanze denominate devono essere ricreate su richiesta.

IOptionsChangeTokenSource<TOptions> consente di recuperare l'oggetto IChangeToken che tiene traccia delle modifiche apportate all'istanza di TOptions sottostante. Per altre informazioni sulle primitive dei token di modifica, vedere Modificare le notifiche.

Vantaggi delle interfacce options

L'uso di un tipo wrapper generico consente di separare la durata dell'opzione dal contenitore di inserimento delle dipendenze. L'interfaccia IOptions<TOptions>.Value fornisce un livello di astrazione, inclusi i vincoli generici, sul tipo di opzioni. Fornisce i seguenti vantaggi:

  • La valutazione dell'istanza di configurazione T viene posticipata all'accesso di IOptions<TOptions>.Value, anziché quando viene inserita. Questo aspetto è importante perché è possibile utilizzare l'opzione T da varie posizioni e scegliere la semantica di durata senza modificare nulla in merito a T.
  • Quando si registrano opzioni di tipo T, non è necessario registrare in modo esplicito il tipo T. Questo è molto utile quando si crea una libreria con impostazioni predefinite semplici e non si vuole forzare il chiamante a registrare le opzioni nel contenitore di inserimento delle dipendenze con una durata specifica.
  • Dal punto di vista dell'API, consente vincoli sul tipo T (in questo caso, T è vincolato a un tipo riferimento).

Usare IOptionsSnapshot per leggere i dati aggiornati

Quando si usa IOptionsSnapshot<TOptions>, le opzioni vengono calcolate una volta per richiesta quando viene eseguito l'accesso e la memorizzazione nella cache per la durata della richiesta. Le modifiche alla configurazione vengono lette dopo l'avvio dell'app quando si usano provider di configurazione che supportano la lettura dei valori di configurazione aggiornati.

Differenze tra IOptionsMonitor e IOptionsSnapshot:

  • IOptionsMonitor è un servizio singleton che recupera i valori delle opzioni correnti in qualsiasi momento, particolarmente utile nelle dipendenze singleton.
  • IOptionsSnapshot è un servizio con ambito e fornisce uno snapshot delle opzioni al momento della costruzione dell'oggetto IOptionsSnapshot<T>. Gli snapshot delle opzioni sono progettati per l'uso con dipendenze temporanee e con ambito.

Il codice seguente usa 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}");
    }
}

Il codice seguente registra un'istanza di configurazione che TransientFaultHandlingOptions associa a:

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

Nel codice precedente, il metodo Configure<TOptions> viene usato per registrare un'istanza di configurazione per l'associazione di TOptions e aggiorna le opzioni quando la configurazione cambia.

IOptionsMonitor

Per usare il monitoraggio delle opzioni, gli oggetti options vengono configurati nello stesso modo in una sezione di configurazione.

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

Nell'esempio seguente viene usato 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}");
    }
}

Nel codice precedente le modifiche al file di configurazione JSON dopo l'avvio dell'app vengono lette.

Suggerimento

È possibile che alcuni file system, ad esempio i contenitori Docker e le condivisioni di rete, non inviino sempre le notifiche di modifica. Quando si usa l'interfaccia IOptionsMonitor<TOptions> in questi ambienti, impostare la variabile di ambiente DOTNET_USE_POLLING_FILE_WATCHER su 1 o true per eseguire il polling del file system per le modifiche. L'intervallo di polling per le modifiche è ogni quattro secondi e non è configurabile.

Per altre informazioni sui contenitori Docker, vedere Distribuire un'app .NET in contenitori.

Supporto delle opzioni denominate con IConfigureNamedOptions

Le opzioni denominate:

  • Sono utili quando più sezioni di configurazione sono associate alle stesse proprietà.
  • Fanno distinzione tra maiuscole e minuscole.

Considerare il file appsettings.json seguente:

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

Anziché creare due classi per associare Features:Personalize e Features:WeatherStation, viene usata la classe seguente per ogni sezione:

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

Nel codice seguente vengono configurate le opzioni denominate:

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

Il codice seguente mostra le opzioni denominate:

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

Tutte le opzioni sono istanze denominate. Le istanze di IConfigureOptions<TOptions> sono considerate come destinate all'istanza di Options.DefaultName, ovvero string.Empty. IConfigureNamedOptions<TOptions> implementa anche IConfigureOptions<TOptions>. L'implementazione predefinita di IOptionsFactory<TOptions> include la logica per usarle in modo appropriato. L'opzione denominata null viene usata per avere come destinazione tutte le istanze denominate anziché un'istanza denominata specifica. Questa convenzione è usata da ConfigureAll e PostConfigureAll.

API OptionsBuilder

OptionsBuilder<TOptions> viene usata per configurare le istanze di TOptions. OptionsBuilder semplifica la creazione di opzioni denominate perché è costituita da un singolo parametro nella chiamata iniziale ad AddOptions<TOptions>(string optionsName) invece di essere visualizzata in tutte le chiamate successive. La convalida delle opzioni e gli overload ConfigureOptions che accettano le dipendenze dei servizi sono disponibili solo tramite OptionsBuilder.

OptionsBuilder è usata nella sezione Convalida delle opzioni.

Usare i servizi di inserimento delle dipendenze per configurare le opzioni

Per accedere ai servizi dall'inserimento delle dipendenze durante la configurazione delle opzioni è possibile procedere in due modi:

  • Passare un delegato di configurazione in Configure in OptionsBuilder<TOptions >>. OptionsBuilder<TOptions> fornisce overload di Configure che consentono di usare fino a cinque servizi per configurare le opzioni:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Creare un tipo che implementa IConfigureOptions<TOptions> o IConfigureNamedOptions<TOptions> e registrare il tipo come servizio.

È consigliabile passare un delegato di configurazione a Configure perché la creazione di un servizio è più complessa. La creazione di un tipo equivale alle operazioni che il framework esegue quando chiama Configure. La chiamata a Configure registra un'interfaccia IConfigureNamedOptions<TOptions> generica temporanea, che ha un costruttore che accetta i tipi di servizi generici specificati.

Convalida delle opzioni

La convalida delle opzioni consente di convalidare i valori delle opzioni.

Considerare il file appsettings.json seguente:

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

La classe seguente viene associata alla sezione di configurazione "MyCustomSettingsSection" e applica un paio di regole DataAnnotations:

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

Nella classe SettingsOptions precedente, la proprietà ConfigurationSectionName contiene il nome della sezione di configurazione a cui eseguire l'associazione. In questo scenario, l'oggetto opzioni fornisce il nome della relativa sezione di configurazione.

Suggerimento

Il nome della sezione di configurazione è indipendente dall'oggetto di configurazione a cui viene associata. In altre parole, una sezione di configurazione denominata "FooBarOptions" può essere associata a un oggetto options denominato ZedOptions. Sebbene assegnare lo stesso nome sia una prassi comune, non è necessario e possono effettivamente verificarsi conflitti tra nomi.

Il codice seguente:

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

Il metodo di estensione ValidateDataAnnotations viene definito nel pacchetto NuGet Microsoft.Extensions.Options.DataAnnotations.

Il codice seguente visualizza i valori di configurazione o segnala gli errori di convalida:

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

Il codice seguente applica una regola di convalida più complessa usando un delegato:

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

La convalida viene eseguita in fase di esecuzione, ma è possibile configurarla in modo che venga eseguita all'avvio concatenando una chiamata a 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();

A partire da .NET 8, è possibile usare un'API alternativa, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), che abilita la convalida all'avvio per un tipo options specifico:

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 per la convalida complessa

La classe seguente implementa 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 consente lo spostamento del codice di convalida in una classe.

Nota

Questo codice di esempio si basa sul pacchetto NuGet Microsoft.Extensions.Configuration.Json.

Usando il codice precedente, la convalida viene abilitata quando si configurano i servizi con il codice seguente:

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

Post-configurazione delle opzioni

Impostare la post-configurazione con IPostConfigureOptions<TOptions>. La post-configurazione viene eseguita dopo tutte le operazioni di configurazione IConfigureOptions<TOptions> e può essere utile negli scenari in cui è necessario eseguire l'override della configurazione:

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

PostConfigure è disponibile per la post-configurazione delle opzioni denominate:

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

Usare PostConfigureAll per la post-configurazione di tutte le istanze di configurazione:

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

Vedi anche