Padrão de opções no ASP.NET Core

Por Kirk Larkin e Rick Anderson.

O padrão de opções usa classes para fornecer acesso fortemente tipado a grupos de configurações relacionadas. Quando as definições de configuração são isoladas por cenário em classes separadas, o aplicativo segue dois princípios importantes de engenharia de software:

  • Encapsulamento:
    • As classes que dependem das configurações dependem apenas das configurações que elas usam.
  • Separação de preocupações:
    • Configurações para diferentes partes do aplicativo não são dependentes ou acoplados uns aos outros.

As opções também fornecem um mecanismo para validar os dados da configuração. Para obter mais configurações, consulte a seção Validação de opções.

Este artigo fornece informações sobre o padrão de opções em ASP.NET Core. Para obter informações sobre como usar o padrão de opções em aplicativos de console, consulte o padrão Opções no .NET.

Associar configuração hierárquica

A maneira preferida de ler valores de configuração relacionados é usando o padrão de opções. Por exemplo, para ler os seguintes valores de configuração:

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

Crie a seguinte PositionOptions classe:

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

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

Uma classe de opções:

  • Deve ser não abstrato com um construtor público sem parâmetros.
  • Todas as propriedades públicas de leitura e gravação do tipo estão associadas.
  • Os campos não estão associados. No código anterior, Position não está associado. O Position campo é usado para que a cadeia "Position" de caracteres não precise ser codificada no aplicativo ao associar a classe a um provedor de configuração.

O seguinte código:

  • Chama ConfigurationBinder.Bind para associar a PositionOptions classe à Position seção.
  • Exibe os dados de Position configuração.
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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

ConfigurationBinder.Get<T> associa e retorna o tipo especificado. ConfigurationBinder.Get<T> pode ser mais conveniente do que usar ConfigurationBinder.Bind. O código a seguir mostra como usar ConfigurationBinder.Get<T> com a PositionOptions classe:

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

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

Uma abordagem alternativa ao usar o padrão de opções é associar a Position seção e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, PositionOptions é adicionado ao contêiner de serviço com Configure e associado à configuração:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

Usando o código anterior, o código a seguir lê as opções de posição:

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

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo não são lidas. Para ler as alterações após o início do aplicativo, use IOptionsSnapshot.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Cenários pós-configuração habilitam a configuração ou a alteração de opções depois que todas as IConfigureOptions<TOptions> configurações ocorrem.

O IOptionsFactory<TOptions> é responsável por criar novas instâncias de opções. Ele tem um único método Create. A implementação padrão usa todos os IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> registrados e executa todas as configurações primeiro, seguidas da pós-configuração. Ela faz distinção entre IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> e chama apenas a interface apropriada.

O IOptionsMonitorCache<TOptions> é usado pelo IOptionsMonitor<TOptions> para armazenar em cache as instâncias do TOptions. O IOptionsMonitorCache<TOptions> invalida as instâncias de opções no monitor, de modo que o valor seja recalculado (TryRemove). Os valores podem ser manualmente inseridos com TryAdd. O método Clear é usado quando todas as instâncias nomeadas devem ser recriadas sob demanda.

Usar IOptionsSnapshot para ler dados atualizados

Usando IOptionsSnapshot<TOptions>:

  • As opções são calculadas uma vez por solicitação, quando acessadas e armazenadas em cache durante o tempo de vida da solicitação.
  • Pode incorrer em uma penalidade significativa de desempenho porque é um serviço com escopo e é recomputado por solicitação. Para obter mais informações, consulte este GitHub problema e melhore o desempenho da associação de configuração.
  • As alterações na configuração são lidas depois que o aplicativo é iniciado ao usar provedores de configuração que dão suporte à leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço Singleton que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências singleton.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o IOptionsSnapshot<T> objeto é construído. Os instantâneos de opções são projetados para uso com dependências transitórias e com escopo.

O código a seguir 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}");
    }
}

O código a seguir registra uma instância de configuração que MyOptions se 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();

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

IOptionsMonitor

O código a seguir registra uma instância de configuração que MyOptions se associa.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

O exemplo a seguir usa 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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

Suporte a opções nomeadas usando IConfigureNamedOptions

Opções nomeadas:

  • São úteis quando várias seções de configuração se associam às mesmas propriedades.
  • Diferencia maiúsculas de minúsculas.

Considere o seguinte appsettings.json arquivo:

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

Em vez de criar duas classes para associar TopItem:Month e TopItem:Year, a seguinte classe é usada para cada seção:

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

O código a seguir configura as opções nomeadas:

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

O código a seguir exibe as opções nomeadas:

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

Todas as opções são instâncias nomeadas. IConfigureOptions<TOptions> as instâncias são tratadas como direcionamento à Options.DefaultName instância, que é string.Empty. IConfigureNamedOptions<TOptions> também implementa IConfigureOptions<TOptions>. A implementação padrão de IOptionsFactory<TOptions> tem lógica para usar cada um de forma adequada. A null opção nomeada é usada para direcionar todas as instâncias nomeadas em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll use esta convenção.

API OptionsBuilder

OptionsBuilder<TOptions> é usada para configurar instâncias TOptions. OptionsBuilder simplifica a criação de opções nomeadas, pois é apenas um único parâmetro para a chamada AddOptions<TOptions>(string optionsName) inicial, em vez de aparecer em todas as chamadas subsequentes. A validação de opções e as sobrecargas ConfigureOptions que aceitam dependências de serviço só estão disponíveis por meio de OptionsBuilder.

OptionsBuilder é usado na seção Validação de Opções .

Consulte Usar AddOptions para configurar o repositório personalizado para obter informações adicionando um repositório personalizado.

Usar os serviços de injeção de dependência para configurar as opções

Os serviços podem ser acessados por meio da injeção de dependência ao configurar opções de duas maneiras:

  • Passe um delegado de configuração paraConfigure.OptionsBuilder<TOptions> OptionsBuilder<TOptions> fornece sobrecargas que Configure permitem o uso de até cinco serviços para configurar opções:

    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));
    
  • Crie um tipo que implemente IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> registre o tipo como um serviço.

É recomendável passar um delegado de configuração para Configure, já que a criação de um serviço é mais complexa. A criação de um tipo é equivalente ao que a estrutura faz ao chamar Configure. A chamada Configure registra um genérico IConfigureNamedOptions<TOptions>transitório, que tem um construtor que aceita os tipos de serviço genéricos especificados.

Validação de opções

A validação de opções permite que valores de opção sejam validados.

Considere o seguinte appsettings.json arquivo:

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

A seguinte classe é usada para associar à "MyConfig" seção de configuração e aplica algumas DataAnnotations regras:

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

O seguinte código:

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

O ValidateDataAnnotations método de extensão é definido no pacote NuGet Microsoft.Extensions.Options.DataAnnotations. Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, esse pacote é referenciado implicitamente da estrutura compartilhada.

O código a seguir exibe os valores de configuração ou os erros de validação:

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

O código a seguir aplica uma regra de validação mais complexa usando um delegado:

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

A classe a seguir 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 permite mover o código de validação de Program.cs e para uma classe.

Usando o código anterior, a validação está habilitada Program.cs com o seguinte código:

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

A validação de opções também dá IValidatableObjectsuporte. Para executar a validação em nível de classe de uma classe dentro da própria classe:

ValidateOnStart

A validação de opções é executada na primeira vez em que IOptionsSnapshot<TOptions>uma implementação ou IOptionsMonitor<TOptions> uma IOptions<TOptions>implementação é criada. Para executar a validação de opções ansiosamente, quando o aplicativo for iniciado, chame ValidateOnStartProgram.cs:

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

Pós-configuração de opções

Defina a pós-configuração com IPostConfigureOptions<TOptions>. A pós-configuração é executada depois que toda o configuração de IConfigureOptions<TOptions> é feita:

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

O PostConfigure está disponível para pós-configurar opções nomeadas:

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

Use PostConfigureAll para pós-configurar todas as instâncias de configuração:

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

Opções de acesso em Program.cs

Para acessar IOptions<TOptions> ou IOptionsMonitor<TOptions> entrarProgram.cs, ligue:WebApplication.ServicesGetRequiredService

var app = builder.Build();

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

Recursos adicionais

Por Kirk Larkin e Rick Anderson.

O padrão de opções usa classes para fornecer acesso fortemente tipado a grupos de configurações relacionadas. Quando as definições de configuração são isoladas por cenário em classes separadas, o aplicativo segue dois princípios importantes de engenharia de software:

  • Encapsulamento:
    • As classes que dependem das configurações dependem apenas das configurações que elas usam.
  • Separação de preocupações:
    • Configurações para diferentes partes do aplicativo não são dependentes ou acoplados uns aos outros.

As opções também fornecem um mecanismo para validar os dados da configuração. Para obter mais configurações, consulte a seção Validação de opções.

Este tópico fornece informações sobre o padrão de opções em ASP.NET Core. Para obter informações sobre como usar o padrão de opções em aplicativos de console, consulte o padrão Opções no .NET.

Exibir ou baixar código de exemplo (como baixar)

Associar configuração hierárquica

A maneira preferida de ler valores de configuração relacionados é usando o padrão de opções. Por exemplo, para ler os seguintes valores de configuração:

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

Crie a seguinte PositionOptions classe:

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

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

Uma classe de opções:

  • Deve ser não abstrato com um construtor público sem parâmetros.
  • Todas as propriedades públicas de leitura e gravação do tipo estão associadas.
  • Os campos não estão associados. No código anterior, Position não está associado. A Position propriedade é usada para que a cadeia "Position" de caracteres não precise ser codificada no aplicativo ao associar a classe a um provedor de configuração.

O seguinte código:

  • Chama ConfigurationBinder.Bind para associar a PositionOptions classe à Position seção.
  • Exibe os dados de Position configuração.
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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

ConfigurationBinder.Get<T> associa e retorna o tipo especificado. ConfigurationBinder.Get<T> pode ser mais conveniente do que usar ConfigurationBinder.Bind. O código a seguir mostra como usar ConfigurationBinder.Get<T> com a PositionOptions classe:

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

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

Uma abordagem alternativa ao usar o padrão de opções é associar a Position seção e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, PositionOptions é adicionado ao contêiner de serviço com Configure e associado à configuração:

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

Usando o código anterior, o código a seguir lê as opções de posição:

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

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo não são lidas. Para ler as alterações após o início do aplicativo, use IOptionsSnapshot.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Cenários pós-configuração habilitam a configuração ou a alteração de opções depois que todas as IConfigureOptions<TOptions> configurações ocorrem.

O IOptionsFactory<TOptions> é responsável por criar novas instâncias de opções. Ele tem um único método Create. A implementação padrão usa todos os IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> registrados e executa todas as configurações primeiro, seguidas da pós-configuração. Ela faz distinção entre IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> e chama apenas a interface apropriada.

O IOptionsMonitorCache<TOptions> é usado pelo IOptionsMonitor<TOptions> para armazenar em cache as instâncias do TOptions. O IOptionsMonitorCache<TOptions> invalida as instâncias de opções no monitor, de modo que o valor seja recalculado (TryRemove). Os valores podem ser manualmente inseridos com TryAdd. O método Clear é usado quando todas as instâncias nomeadas devem ser recriadas sob demanda.

Usar IOptionsSnapshot para ler dados atualizados

Usando IOptionsSnapshot<TOptions>, as opções são computadas uma vez por solicitação quando acessadas e armazenadas em cache durante o tempo de vida da solicitação. As alterações na configuração são lidas depois que o aplicativo é iniciado ao usar provedores de configuração que dão suporte à leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço Singleton que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências singleton.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o IOptionsSnapshot<T> objeto é construído. Os instantâneos de opções são projetados para uso com dependências transitórias e com escopo.

O código a seguir 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}");
    }
}

O código a seguir registra uma instância de configuração que MyOptions se associa a:

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

    services.AddRazorPages();
}

No código anterior, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

IOptionsMonitor

O código a seguir registra uma instância de configuração que MyOptions se associa.

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

    services.AddRazorPages();
}

O exemplo a seguir usa 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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após o início do aplicativo são lidas.

Suporte a opções nomeadas usando IConfigureNamedOptions

Opções nomeadas:

  • São úteis quando várias seções de configuração se associam às mesmas propriedades.
  • Diferencia maiúsculas de minúsculas.

Considere o seguinte appsettings.json arquivo:

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

Em vez de criar duas classes para associar TopItem:Month e TopItem:Year, a seguinte classe é usada para cada seção:

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

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

O código a seguir configura as opções nomeadas:

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

O código a seguir exibe as opções nomeadas:

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

Todas as opções são instâncias nomeadas. IConfigureOptions<TOptions> as instâncias são tratadas como direcionamento à Options.DefaultName instância, que é string.Empty. IConfigureNamedOptions<TOptions> também implementa IConfigureOptions<TOptions>. A implementação padrão de IOptionsFactory<TOptions> tem lógica para usar cada um de forma adequada. A null opção nomeada é usada para direcionar todas as instâncias nomeadas em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll use esta convenção.

API OptionsBuilder

OptionsBuilder<TOptions> é usada para configurar instâncias TOptions. OptionsBuilder simplifica a criação de opções nomeadas, pois é apenas um único parâmetro para a chamada AddOptions<TOptions>(string optionsName) inicial, em vez de aparecer em todas as chamadas subsequentes. A validação de opções e as sobrecargas ConfigureOptions que aceitam dependências de serviço só estão disponíveis por meio de OptionsBuilder.

OptionsBuilder é usado na seção Validação de Opções .

Consulte Usar AddOptions para configurar o repositório personalizado para obter informações adicionando um repositório personalizado.

Usar os serviços de injeção de dependência para configurar as opções

Os serviços podem ser acessados por meio da injeção de dependência ao configurar opções de duas maneiras:

  • Passe um delegado de configuração paraConfigure.OptionsBuilder<TOptions> OptionsBuilder<TOptions> fornece sobrecargas que Configure permitem o uso de até cinco serviços para configurar opções:

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Crie um tipo que implemente IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> registre o tipo como um serviço.

É recomendável passar um delegado de configuração para Configure, já que a criação de um serviço é mais complexa. A criação de um tipo é equivalente ao que a estrutura faz ao chamar Configure. A chamada Configure registra um genérico IConfigureNamedOptions<TOptions>transitório, que tem um construtor que aceita os tipos de serviço genéricos especificados.

Validação de opções

A validação de opções permite que valores de opção sejam validados.

Considere o seguinte appsettings.json arquivo:

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

A classe a "MyConfig" seguir associa-se à seção de configuração e aplica algumas DataAnnotations regras:

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

O seguinte código:

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

O ValidateDataAnnotations método de extensão é definido no pacote NuGet Microsoft.Extensions.Options.DataAnnotations. Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, esse pacote é referenciado implicitamente da estrutura compartilhada.

O código a seguir exibe os valores de configuração ou os erros de validação:

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

O código a seguir aplica uma regra de validação mais complexa usando um delegado:

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 para validação complexa

A classe a seguir 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 permite mover o código de validação de StartUp e para uma classe.

Usando o código anterior, a validação está habilitada Startup.ConfigureServices com o seguinte código:

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

Pós-configuração de opções

Defina a pós-configuração com IPostConfigureOptions<TOptions>. A pós-configuração é executada depois que toda o configuração de IConfigureOptions<TOptions> é feita:

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

O PostConfigure está disponível para pós-configurar opções nomeadas:

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

Use PostConfigureAll para pós-configurar todas as instâncias de configuração:

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

Acessando opções durante a inicialização

IOptions<TOptions> e IOptionsMonitor<TOptions> podem ser usados em Startup.Configure, pois os serviços são criados antes da execução do método Configure.

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

Não use IOptions<TOptions> ou IOptionsMonitor<TOptions> em Startup.ConfigureServices. Pode haver um estado inconsistente de opções devido à ordenação dos registros de serviço.

Pacote NuGet Options.ConfigurationExtensions

O pacote Microsoft.Extensions.Options.ConfigurationExtensions é referenciado implicitamente em aplicativos ASP.NET Core.