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

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Por 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:
    • Classes que dependem de definições de configuração dependem apenas das definições de configuração usadas por elas.
  • Separação de Interesses:
    • As configurações para diferentes partes do aplicativo não são dependentes nem acopladas entre si.

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.

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

Associar configuração hierárquica

A maneira preferencial de ler os 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 classe PositionOptions:

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.
  • Tem propriedades públicas de leitura/gravação do tipo que têm itens correspondentes na configuração que são associadas.
  • Tem propriedades de leitura/gravação associadas às entradas correspondentes na configuração.
  • Não tem os campos associados. No código anterior, Position não está vinculado. O campo Position é usado para que a cadeia de caracteres "Position" 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 classe PositionOptions à seção Position.
  • Exibe os dados de configuração de 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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após a inicialização 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 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}");
    }
}

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

A associação também permite a concretização de uma classe abstrata. Considere o código a seguir que usa a classe abstrata SomethingWithAName:

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

O código a seguir exibe os valores de configuração NameTitleOptions:

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

As chamadas para Bind são menos restritas do que as chamadas para Get<>:

  • Bind permite a concretização de um resumo.
  • Get<> tem que criar uma instância própria.

O padrão de opções

Uma abordagem alternativa ao usar o padrão de opções é associar a seção Position e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, PositionOptions é adicionada ao contêiner de serviço com Configure e associada à 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 a inicialização do aplicativo não são lidas. Para ler as alterações após a inicialização do aplicativo, use IOptionsSnapshot.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Os cenários de Pós-configuração permitem definir ou alterar as opções depois de todas as configurações do IConfigureOptions<TOptions> serem feitas.

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 é computado novamente por solicitação. Para obter mais informações, consulte este problema do GitHub e Aprimorar 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 permitem a leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço de banco de dados individual que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências de banco de dados individual.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o objeto IOptionsSnapshot<T> é 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 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 aplicativo iniciar são lidas.

IOptionsMonitor

O código a seguir registra uma instância de configuração a 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 a inicialização do aplicativo são lidas.

Compatibilidade de 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 arquivo appsettings.json:

{
  "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 mostra 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. As instâncias IConfigureOptions<TOptions> existentes são tratadas como sendo direcionadas à instância Options.DefaultName, 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 opção nomeada null é usada para direcionar todas as instâncias nomeadas, em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll usam essa 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 sobre como adicionar 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 de injeção de dependência ao configurar as opções de duas maneiras:

  • Aprova um representante de configuração para Configure em OptionsBuilder<TOptions>. OptionsBuilder<TOptions> oferece sobrecargas de Configure que permitem usar 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));
    
  • Criar um tipo que implementa IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registra o tipo como um serviço.

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

Validação de opções

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

Considere o seguinte arquivo appsettings.json:

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

A classe a seguir é usada para se associar à seção de configuração "MyConfig" e aplica algumas regras de 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; }
}

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 método de extensão ValidateDataAnnotations é definido no pacote Microsoft.Extensions.Options.DataAnnotations do NuGet. Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, 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 representante:

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 retirar o código de validação do Program.cs e colocar em uma classe.

Usando o código anterior, a validação está habilitada em 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á suporte a IValidatableObject. Para executar a validação de nível de classe de uma classe dentro da própria classe:

ValidateOnStart

A validação de opções é executada na primeira vez que uma implementação IOptions<TOptions>, IOptionsSnapshot<TOptions>ou IOptionsMonitor<TOptions> é criada. Para executar rapidamente a validação de opções, quando o aplicativo for iniciado, chame ValidateOnStart em Program.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 no Program.cs

Para acessar IOptions<TOptions> ou IOptionsMonitor<TOptions> em Program.cs, chame GetRequiredService em WebApplication.Services:

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:
    • Classes que dependem de definições de configuração dependem apenas das definições de configuração usadas por elas.
  • Separação de Interesses:
    • As configurações para diferentes partes do aplicativo não são dependentes nem acopladas entre si.

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.

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

Associar configuração hierárquica

A maneira preferencial de ler os 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 classe PositionOptions:

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 abstrata e com um construtor público sem parâmetros.
  • Todas as propriedades públicas de leitura e gravação do tipo são vinculadas.
  • Os campos não são vinculados. No código anterior, Position não está vinculado. O campo Position é usado para que a cadeia de caracteres "Position" 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 classe PositionOptions à seção Position.
  • Exibe os dados de configuração de 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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após a inicialização 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 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}");
    }
}

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

Uma abordagem alternativa ao usar o padrão de opções é associar a seção Position e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, PositionOptions é adicionada ao contêiner de serviço com Configure e associada à 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 a inicialização do aplicativo não são lidas. Para ler as alterações após a inicialização do aplicativo, use IOptionsSnapshot.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Os cenários de Pós-configuração permitem definir ou alterar as opções depois de todas as configurações do IConfigureOptions<TOptions> serem feitas.

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 é computado novamente por solicitação. Para obter mais informações, consulte este problema do GitHub e Aprimorar 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 permitem a leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço de banco de dados individual que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências de banco de dados individual.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o objeto IOptionsSnapshot<T> é 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 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 aplicativo iniciar são lidas.

IOptionsMonitor

O código a seguir registra uma instância de configuração a 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 a inicialização do aplicativo são lidas.

Compatibilidade de 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 arquivo appsettings.json:

{
  "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 mostra 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. As instâncias IConfigureOptions<TOptions> existentes são tratadas como sendo direcionadas à instância Options.DefaultName, 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 opção nomeada null é usada para direcionar todas as instâncias nomeadas, em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll usam essa 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 sobre como adicionar 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 de injeção de dependência ao configurar as opções de duas maneiras:

  • Aprova um representante de configuração para Configure em OptionsBuilder<TOptions>. OptionsBuilder<TOptions> oferece sobrecargas de Configure que permitem usar 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));
    
  • Criar um tipo que implementa IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registra o tipo como um serviço.

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

Validação de opções

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

Considere o seguinte arquivo appsettings.json:

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

A classe a seguir é usada para se associar à seção de configuração "MyConfig" e aplica algumas regras de 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; }
}

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 método de extensão ValidateDataAnnotations é definido no pacote Microsoft.Extensions.Options.DataAnnotations do NuGet. Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, 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 representante:

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 retirar o código de validação do Program.cs e colocar em uma classe.

Usando o código anterior, a validação está habilitada em 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á suporte a IValidatableObject. Para executar a validação de nível de classe de uma classe dentro da própria classe:

ValidateOnStart

A validação de opções é executada na primeira vez que uma implementação IOptions<TOptions>, IOptionsSnapshot<TOptions>ou IOptionsMonitor<TOptions> é criada. Para executar rapidamente a validação de opções, quando o aplicativo for iniciado, chame ValidateOnStart em Program.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 no Program.cs

Para acessar IOptions<TOptions> ou IOptionsMonitor<TOptions> em Program.cs, chame GetRequiredService em WebApplication.Services:

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:
    • Classes que dependem de definições de configuração dependem apenas das definições de configuração usadas por elas.
  • Separação de Interesses:
    • As configurações para diferentes partes do aplicativo não são dependentes nem acopladas entre si.

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.

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

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

Associar configuração hierárquica

A maneira preferencial de ler os 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 classe PositionOptions:

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 abstrata e com um construtor público sem parâmetros.
  • Todas as propriedades públicas de leitura e gravação do tipo são vinculadas.
  • Os campos não são vinculados. No código anterior, Position não está vinculado. A propriedade Position é usada para que a cadeia de caracteres "Position" 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 classe PositionOptions à seção Position.
  • Exibe os dados de configuração de 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}");
    }
}

No código anterior, por padrão, as alterações no arquivo de configuração JSON após a inicialização 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 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}");
    }
}

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

Uma abordagem alternativa ao usar o padrão de opções é associar a seção Position e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, PositionOptions é adicionada ao contêiner de serviço com Configure e associada à 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 a inicialização do aplicativo não são lidas. Para ler as alterações após a inicialização do aplicativo, use IOptionsSnapshot.

Interfaces de opções

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Os cenários de Pós-configuração permitem definir ou alterar as opções depois de todas as configurações do IConfigureOptions<TOptions> serem feitas.

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 permitem a leitura de valores de configuração atualizados.

A diferença entre IOptionsMonitor e IOptionsSnapshot é que:

  • IOptionsMonitor é um serviço de banco de dados individual que recupera valores de opção atuais a qualquer momento, o que é especialmente útil em dependências de banco de dados individual.
  • IOptionsSnapshot é um serviço com escopo e fornece um instantâneo das opções no momento em que o objeto IOptionsSnapshot<T> é 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 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 aplicativo iniciar são lidas.

IOptionsMonitor

O código a seguir registra uma instância de configuração a 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 a inicialização do aplicativo são lidas.

Compatibilidade de 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 arquivo appsettings.json:

{
  "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 mostra 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. As instâncias IConfigureOptions<TOptions> existentes são tratadas como sendo direcionadas à instância Options.DefaultName, 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 opção nomeada null é usada para direcionar todas as instâncias nomeadas, em vez de uma instância nomeada específica. ConfigureAll e PostConfigureAll usam essa 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 sobre como adicionar 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 de injeção de dependência ao configurar as opções de duas maneiras:

  • Aprova um representante de configuração para Configure em OptionsBuilder<TOptions>. OptionsBuilder<TOptions> oferece sobrecargas de Configure que permitem usar 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));
    
  • Criar um tipo que implementa IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registra o tipo como um serviço.

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

Validação de opções

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

Considere o seguinte arquivo appsettings.json:

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

A classe a seguir associa-se à seção de configuração "MyConfig" e aplica algumas regras de 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; }
}

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 método de extensão ValidateDataAnnotations é definido no pacote Microsoft.Extensions.Options.DataAnnotations do NuGet. Para aplicativos Web que usam o SDK Microsoft.NET.Sdk.Web, 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 representante:

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 retirar o código de validação do StartUp e colocar em uma classe.

Usando o código anterior, a validação está habilitada em 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.