Condividi tramite


Modello di opzioni in ASP.NET Core

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Di Rick Anderson.

Il modello di opzioni usa classi per fornire l'accesso fortemente tipizzato ai 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:

  • Incapsulamento:
    • Le classi che dipendono dalle impostazioni di configurazione dipendono solo dalle impostazioni di configurazione usate.
  • Separazione dei problemi:
    • Le impostazioni per parti diverse dell'app non sono dipendenti o associate l'una all'altra.

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

Questo articolo fornisce informazioni sul modello di opzioni in ASP.NET Core. Per informazioni sull'uso del modello di opzioni nelle app console, vedere Modello di opzioni in .NET.

Associare la configurazione gerarchica

Il modo preferito per leggere i valori di configurazione correlati prevede l'uso del modello di opzioni. Ad esempio, per leggere i valori di configurazione seguenti:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Creare la classe PositionOptions seguente:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Una classe di opzioni:

  • Deve essere non astratto.
  • Dispone di proprietà pubbliche di lettura/scrittura del tipo con elementi corrispondenti nella configurazione sono associate.
  • Dispone delle proprietà di lettura/scrittura associate alle voci corrispondenti nella configurazione.
  • I campi non sono associati. Nel codice precedente Position non è associato. Il campo Position viene usato in modo che la stringa "Position" non debba essere hardcoded nell'app quando la classe viene associata a un provider di configurazione.

Il codice seguente:

  • Chiama ConfigurationBinder.Bind per associare la classe PositionOptions alla sezione Position.
  • Visualizza i dati di configurazione di Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

Bind consente anche la concretezza di una classe astratta. Si consideri il codice seguente che usa la classe SomethingWithANameastratta :

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

Nel codice seguente vengono visualizzati i valori di NameTitleOptions configurazione:

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

Le chiamate a Bind sono meno rigide delle chiamate a Get<>:

  • Bind consente la concretezza di un'astrazione.
  • Get<> deve creare un'istanza stessa.

Modello di opzioni

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

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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.

Interfacce per le opzioni

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Gli scenari di post-configurazione abilitano l'impostazione o la modifica delle opzioni dopo che si verifica tutta la IConfigureOptions<TOptions> configurazione.

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.

Usare IOptionsSnapshot per leggere i dati aggiornati

Utilizzo di 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.
  • Può comportare una riduzione significativa delle prestazioni perché si tratta di un servizio con ambito e viene ricalcorato per ogni richiesta. Per altre informazioni, vedere questo problema di GitHub e Migliorare le prestazioni dell'associazione di configurazione.
  • 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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

IOptionsMonitor

Il codice seguente registra un'istanza di configurazione che MyOptions viene associata a .

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Nell'esempio seguente viene usato IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

Supporto delle opzioni denominate con IConfigureNamedOptions

Le opzioni denominate:

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

Considerare il file appsettings.json seguente:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anziché creare due classi per associare TopItem:Month e TopItem:Year, viene usata la classe seguente per ogni sezione:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

Nel codice seguente vengono configurate le opzioni denominate:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

Il codice seguente mostra le opzioni denominate:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Per informazioni sull'aggiunta di un repository personalizzato, vedere Usare AddOptions.

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 a Configure in OptionsBuilder<TOptions>. OptionsBuilder<TOptions> fornisce overload di Configure che consentono l'uso di un massimo di cinque servizi per configurare le opzioni:

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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 a ciò che il framework esegue quando si chiama Configure. La chiamata Configure registra un generico IConfigureNamedOptions<TOptions>temporaneo, che dispone di un costruttore che accetta i tipi di servizio generici specificati.

Convalida delle opzioni

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

Considerare il file appsettings.json seguente:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

La classe seguente viene usata per eseguire l'associazione "MyConfig" alla sezione di configurazione e applica un paio di DataAnnotations regole:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Il codice seguente:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Il metodo di estensione ValidateDataAnnotations viene definito nel pacchetto NuGet Microsoft.Extensions.Options.DataAnnotations. Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , questo pacchetto viene fatto riferimento in modo implicito dal framework condiviso.

Nel codice seguente vengono visualizzati i valori di configurazione o gli errori di convalida:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> e IValidatableObject

La classe seguente implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions consente di spostare il codice di Program.cs convalida da e in una classe.

Usando il codice precedente, la convalida è abilitata in Program.cs con il codice seguente:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

La convalida delle opzioni supporta IValidatableObjectanche . Per eseguire la convalida a livello di classe di una classe all'interno della classe stessa:

ValidateOnStart

La convalida delle opzioni viene eseguita la prima volta che viene creata un'istanza TOption di . Ciò significa, ad esempio, quando si verifica il primo accesso a IOptionsSnapshot<TOptions>.Value in una pipeline di richieste o quando IOptionsMonitor<TOptions>.Get(string) viene chiamato sulle impostazioni presenti. Dopo aver ricaricato le impostazioni, la convalida viene eseguita di nuovo. Il runtime di ASP.NET Core usa OptionsCache<TOptions> per memorizzare nella cache l'istanza delle opzioni dopo la creazione.

Per eseguire la convalida delle opzioni in modo eager, all'avvio dell'app, chiamare ValidateOnStart<TOptions>(OptionsBuilder<TOptions>)in Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Post-configurazione delle opzioni

Impostare la post-configurazione con IPostConfigureOptions<TOptions>. La post-configurazione viene eseguita dopo il completamento della configurazione di tutte le IConfigureOptions<TOptions>:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Opzioni di accesso in Program.cs

Per accedere IOptions<TOptions> a o IOptionsMonitor<TOptions> in Program.cs, chiamare GetRequiredService su WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Risorse aggiuntive

Di Kirk Larkin e Rick Anderson.

Il modello di opzioni usa classi per fornire l'accesso fortemente tipizzato ai 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:

  • Incapsulamento:
    • Le classi che dipendono dalle impostazioni di configurazione dipendono solo dalle impostazioni di configurazione usate.
  • Separazione dei problemi:
    • Le impostazioni per parti diverse dell'app non sono dipendenti o associate l'una all'altra.

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

Questo articolo fornisce informazioni sul modello di opzioni in ASP.NET Core. Per informazioni sull'uso del modello di opzioni nelle app console, vedere Modello di opzioni in .NET.

Associare la configurazione gerarchica

Il modo preferito per leggere i valori di configurazione correlati prevede l'uso del modello di opzioni. Ad esempio, per leggere i valori di configurazione seguenti:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Creare la classe PositionOptions seguente:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Una classe di opzioni:

  • Deve essere non astratta con un costruttore pubblico senza parametri.
  • Tutte le proprietà di lettura/scrittura pubbliche del tipo sono associate.
  • I campi non sono associati. Nel codice precedente Position non è associato. Il campo Position viene usato in modo che la stringa "Position" non debba essere hardcoded nell'app quando la classe viene associata a un provider di configurazione.

Il codice seguente:

  • Chiama ConfigurationBinder.Bind per associare la classe PositionOptions alla sezione Position.
  • Visualizza i dati di configurazione di Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

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

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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.

Interfacce per le opzioni

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Gli scenari di post-configurazione abilitano l'impostazione o la modifica delle opzioni dopo che si verifica tutta la IConfigureOptions<TOptions> configurazione.

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.

Usare IOptionsSnapshot per leggere i dati aggiornati

Utilizzo di 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.
  • Può comportare una riduzione significativa delle prestazioni perché si tratta di un servizio con ambito e viene ricalcorato per ogni richiesta. Per altre informazioni, vedere questo problema di GitHub e Migliorare le prestazioni dell'associazione di configurazione.
  • 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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

IOptionsMonitor

Il codice seguente registra un'istanza di configurazione che MyOptions viene associata a .

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Nell'esempio seguente viene usato IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

Supporto delle opzioni denominate con IConfigureNamedOptions

Le opzioni denominate:

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

Considerare il file appsettings.json seguente:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anziché creare due classi per associare TopItem:Month e TopItem:Year, viene usata la classe seguente per ogni sezione:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

Nel codice seguente vengono configurate le opzioni denominate:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

Il codice seguente mostra le opzioni denominate:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Per informazioni sull'aggiunta di un repository personalizzato, vedere Usare AddOptions.

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 a Configure in OptionsBuilder<TOptions>. OptionsBuilder<TOptions> fornisce overload di Configure che consentono l'uso di un massimo di cinque servizi per configurare le opzioni:

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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 a ciò che il framework esegue quando si chiama Configure. La chiamata Configure registra un generico IConfigureNamedOptions<TOptions>temporaneo, che dispone di un costruttore che accetta i tipi di servizio generici specificati.

Convalida delle opzioni

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

Considerare il file appsettings.json seguente:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

La classe seguente viene usata per eseguire l'associazione "MyConfig" alla sezione di configurazione e applica un paio di DataAnnotations regole:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Il codice seguente:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Il metodo di estensione ValidateDataAnnotations viene definito nel pacchetto NuGet Microsoft.Extensions.Options.DataAnnotations. Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , questo pacchetto viene fatto riferimento in modo implicito dal framework condiviso.

Nel codice seguente vengono visualizzati i valori di configurazione o gli errori di convalida:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> e IValidatableObject

La classe seguente implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions consente di spostare il codice di Program.cs convalida da e in una classe.

Usando il codice precedente, la convalida è abilitata in Program.cs con il codice seguente:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

La convalida delle opzioni supporta IValidatableObjectanche . Per eseguire la convalida a livello di classe di una classe all'interno della classe stessa:

ValidateOnStart

La convalida delle opzioni viene eseguita la prima volta che viene creata un'implementazione IOptions<TOptions>, IOptionsSnapshot<TOptions>o IOptionsMonitor<TOptions> . Per eseguire la convalida delle opzioni in modo eager, all'avvio dell'app, chiamare ValidateOnStart in Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Post-configurazione delle opzioni

Impostare la post-configurazione con IPostConfigureOptions<TOptions>. La post-configurazione viene eseguita dopo il completamento della configurazione di tutte le IConfigureOptions<TOptions>:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Opzioni di accesso in Program.cs

Per accedere IOptions<TOptions> a o IOptionsMonitor<TOptions> in Program.cs, chiamare GetRequiredService su WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Risorse aggiuntive

Di Kirk Larkin e Rick Anderson.

Il modello di opzioni usa classi per fornire l'accesso fortemente tipizzato ai 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:

  • Incapsulamento:
    • Le classi che dipendono dalle impostazioni di configurazione dipendono solo dalle impostazioni di configurazione usate.
  • Separazione dei problemi:
    • Le impostazioni per parti diverse dell'app non sono dipendenti o associate l'una all'altra.

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

In questo argomento vengono fornite informazioni sul modello di opzioni in ASP.NET Core. Per informazioni sull'uso del modello di opzioni nelle app console, vedere Modello di opzioni in .NET.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Associare la configurazione gerarchica

Il modo preferito per leggere i valori di configurazione correlati prevede l'uso del modello di opzioni. Ad esempio, per leggere i valori di configurazione seguenti:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Creare la classe PositionOptions seguente:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

Una classe di opzioni:

  • Deve essere non astratta con un costruttore pubblico senza parametri.
  • Tutte le proprietà di lettura/scrittura pubbliche del tipo sono associate.
  • I campi non sono associati. Nel codice precedente Position non è associato. La proprietà Position viene usata in modo che la stringa "Position" non debba essere hardcoded nell'app quando la classe viene associata a un provider di configurazione.

Il codice seguente:

  • Chiama ConfigurationBinder.Bind per associare la classe PositionOptions alla sezione Position.
  • Visualizza i dati di configurazione di Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

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 PositionOptions:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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.

Interfacce per le opzioni

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Gli scenari di post-configurazione abilitano l'impostazione o la modifica delle opzioni dopo che si verifica tutta la IConfigureOptions<TOptions> configurazione.

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.

Usare IOptionsSnapshot per leggere i dati aggiornati

Usando IOptionsSnapshot<TOptions>, le opzioni vengono calcolate una volta per ogni richiesta quando si accede e memorizzato 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>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

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

IOptionsMonitor

Il codice seguente registra un'istanza di configurazione che MyOptions viene associata a .

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

Nell'esempio seguente viene usato IOptionsMonitor<TOptions>:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Nel codice precedente, per impostazione predefinita, le modifiche apportate al file di configurazione JSON dopo l'avvio dell'app vengono lette.

Supporto delle opzioni denominate con IConfigureNamedOptions

Le opzioni denominate:

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

Considerare il file appsettings.json seguente:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anziché creare due classi per associare TopItem:Month e TopItem:Year, viene usata la classe seguente per ogni sezione:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

Nel codice seguente vengono configurate le opzioni denominate:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

Il codice seguente mostra le opzioni denominate:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

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.

Per informazioni sull'aggiunta di un repository personalizzato, vedere Usare AddOptions.

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 a Configure in OptionsBuilder<TOptions>. OptionsBuilder<TOptions> fornisce overload di Configure che consentono l'uso di un massimo di cinque servizi per configurare le opzioni:

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • 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 a ciò che il framework esegue quando si chiama Configure. La chiamata Configure registra un generico IConfigureNamedOptions<TOptions>temporaneo, che dispone di un costruttore che accetta i tipi di servizio generici specificati.

Convalida delle opzioni

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

Considerare il file appsettings.json seguente:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

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

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Il codice seguente:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

Il metodo di estensione ValidateDataAnnotations viene definito nel pacchetto NuGet Microsoft.Extensions.Options.DataAnnotations. Per le app Web che usano l'SDK Microsoft.NET.Sdk.Web , questo pacchetto viene fatto riferimento in modo implicito dal framework condiviso.

Nel codice seguente vengono visualizzati i valori di configurazione o gli errori di convalida:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

IValidateOptions per la convalida complessa

La classe seguente implementa IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions consente di spostare il codice di StartUp convalida da e in una classe.

Usando il codice precedente, la convalida è abilitata in Startup.ConfigureServices con il codice seguente:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

Post-configurazione delle opzioni

Impostare la post-configurazione con IPostConfigureOptions<TOptions>. La post-configurazione viene eseguita dopo il completamento della configurazione di tutte le IConfigureOptions<TOptions>:

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

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

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

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

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Accesso alle opzioni durante l'avvio

IOptions<TOptions> e IOptionsMonitor<TOptions> possono essere usate in Startup.Configure, perché i servizi vengono compilati prima dell'esecuzione del metodo Configure.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

Non usare IOptions<TOptions> oppure IOptionsMonitor<TOptions> in Startup.ConfigureServices. Le opzioni potrebbero avere uno stato incoerente a causa dell'ordinamento delle registrazioni dei servizi.

Pacchetto NuGet Options.ConfigurationExtensions

Il pacchetto Microsoft.Extensions.Options.ConfigurationExtensions viene fatto riferimento in modo implicito nelle app ASP.NET Core.