Padrão de opções no .NET
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:
- O ISP (Princípio de Segregação da Interface) ou Encapsulamento: os cenários (classes) que dependem das definições de configuração dependem apenas das definições de configuração usadas por eles.
- 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.
Associar configuração hierárquica
A maneira preferencial de ler os valores de configuração relacionados é usando o padrão de opções. O padrão de opções é possível por meio da interface IOptions<TOptions>, em que o parâmetro TOptions
de tipo genérico é restrito a um class
. O IOptions<TOptions>
pode ser fornecido posteriormente por meio de injeção de dependência. Para obter mais informações, consulte Injeção de dependência no .NET.
Por exemplo, para ler os valores de configuração destacados de um arquivo appsettings.json:
{
"SecretKey": "Secret key value",
"TransientFaultHandlingOptions": {
"Enabled": true,
"AutoRetryDelay": "00:00:07"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Crie a seguinte classe TransientFaultHandlingOptions
:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
Ao usar o padrão de opções, uma classe de opções:
- Precisa ser não abstrata e ter um construtor público sem parâmetros
- Contém propriedades públicas de leitura e gravação a serem associadas (os campos não estão associados)
O código a seguir faz parte do arquivo C# Program.cs e:
- Chama ConfigurationBinder.Bind para associar a classe
TransientFaultHandlingOptions
à seção"TransientFaultHandlingOptions"
. - Exibe os dados de configuração.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Bind(options);
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
// <Output>
// Sample output:
No código anterior, o arquivo de configuração JSON tem sua seção "TransientFaultHandlingOptions"
associada à instância TransientFaultHandlingOptions
. Isso hidrata as propriedades de objetos C# com os valores correspondentes da configuração.
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 TransientFaultHandlingOptions
:
var options =
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Get<TransientFaultHandlingOptions>();
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
No código anterior, o ConfigurationBinder.Get<T>
é usado para adquirir uma instância do objeto TransientFaultHandlingOptions
com seus valores de propriedade preenchidos usando a configuração subjacente.
Importante
A classe ConfigurationBinder expõe várias APIs, como .Bind(object instance)
e .Get<T>()
que não são restritas a class
. Ao usar qualquer uma das interfaces de opções, você precisa cumprir as restrições de classe de opções mencionadas anteriormente.
Uma abordagem alternativa ao usar o padrão de opções é associar a seção "TransientFaultHandlingOptions"
e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, TransientFaultHandlingOptions
é adicionada ao contêiner de serviço com Configure e associada à configuração:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<TransientFaultHandlingOptions>(
builder.Configuration.GetSection(
key: nameof(TransientFaultHandlingOptions)));
No exemplo anterior, builder
é uma instância de HostApplicationBuilder.
Dica
O parâmetro key
é o nome da seção de configuração a ser pesquisada. Ele não precisa corresponder ao nome do tipo que o representa. Por exemplo, você pode ter uma seção nomeada "FaultHandling"
e ela pode ser representada pela classe TransientFaultHandlingOptions
. Nesse caso, você passará "FaultHandling"
para a GetSection função em vez disso. O operador nameof
é usado como uma conveniência quando a seção nomeada corresponde ao tipo ao qual corresponde.
Usando o código anterior, o código a seguir lê as opções de posição:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
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 o início do aplicativo, use IOptionsSnapshot ou IOptionsMonitor para monitorar as alterações conforme elas ocorrem e reagir adequadamente.
Interfaces de opções
- Não é compatível com:
- Leitura de dados de configuração após o início do aplicativo.
- Opções nomeadas
- É registrado como singleton e pode ser injetado em qualquer tempo de vida do serviço.
- É útil em cenários em que as opções devem ser recalculadas em cada resolução de injeção, em tempos de vida com escopo ou transitórios. Para obter mais informações, consulte Usar IOptionsSnapshot para ler dados atualizados.
- É registrado como Escopo e, portanto, não pode ser injetado em um serviço Singleton.
- Permite opções nomeadas
- O é usado para recuperar as opções e gerenciar notificações de opções para instâncias de
TOptions
. - É registrado como singleton e pode ser injetado em qualquer tempo de vida do serviço.
- Suporte:
- Notificações de alteração
- Opções nomeadas
- Configuração recarregável
- Invalidação seletiva de opções (IOptionsMonitorCache<TOptions>)
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.
IOptionsChangeTokenSource<TOptions> é usado para efetuar fetch de IChangeToken que rastreia as alterações na instância TOptions
subjacente. Para obter mais informações sobre primitivos de token de alteração, consulte Alterar notificações.
Benefícios das interfaces de opções
O uso de um tipo de wrapper genérico oferece a capacidade de desacoplar o tempo de vida da opção do contêiner de DI (injeção de dependência). A interface IOptions<TOptions>.Value fornece uma camada de abstração, incluindo restrições genéricas, em seu tipo de opções. Isso oferece os seguintes benefícios:
- A avaliação da instância de configuração de
T
é adiada para o acesso de , em vez de IOptions<TOptions>.Value, quando ela é injetada. Isso é importante porque você pode consumir a opçãoT
de vários lugares e escolher a semântica de vida sem alterar nada sobreT
. - Ao registrar opções de tipo
T
, você não precisa registrar explicitamente o tipoT
. Essa é uma conveniência quando você está criando uma biblioteca com padrões simples e não quer forçar o chamador a registrar opções no contêiner de DI com um tempo de vida específico. - Do ponto de vista da API, ela permite restrições no tipo
T
(nesse caso,T
é restrita a um tipo de referência).
Usar IOptionsSnapshot para ler dados atualizados
Ao usar 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. 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 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 oIOptionsSnapshot<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>.
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
O código a seguir registra uma instância de configuração que TransientFaultHandlingOptions
associa a:
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
No código anterior, o método Configure<TOptions>
é usado para registrar uma instância de configuração que TOptions
associará, e atualiza as opções quando a configuração é alterada.
IOptionsMonitor
O IOptionsMonitor
tipo dá suporte a notificações de alteração e permite cenários em que seu aplicativo pode precisar responder a alterações de origem de configuração dinamicamente. Isso é útil quando você precisa reagir a alterações nos dados de configuração após a inicialização do aplicativo. As notificações de alteração só têm suporte para provedores de configuração baseados no sistema de arquivos, como os seguintes:
- Microsoft.Extensions.Configuration.Ini
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.KeyPerFile
- Microsoft.Extensions.Configuration.UserSecrets
- Microsoft.Extensions.Configuration.Xml
Para usar o monitor de opções, os objetos de opções são configurados da mesma maneira em uma seção de configuração.
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
O exemplo a seguir usa IOptionsMonitor<TOptions>:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
public void DisplayValues()
{
TransientFaultHandlingOptions options = monitor.CurrentValue;
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
No código anterior, as alterações no arquivo de configuração JSON após o aplicativo iniciar a leitura.
Dica
Alguns sistemas de arquivos, como contêineres do Docker e compartilhamentos de rede, podem não enviar notificações de alteração de forma confiável. Ao usar a interface IOptionsMonitor<TOptions> nesses ambientes, defina a variável de ambiente DOTNET_USE_POLLING_FILE_WATCHER
como 1
ou true
para sondar o sistema de arquivos para obter alterações. O intervalo no qual as alterações são sondadas é a cada quatro segundos e não é configurável.
Para obter mais informações sobre contêineres do Docker, consulte Colocar um aplicativo .NET no contêiner.
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.
Usando o seguinte arquivo appsettings.json:
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
Em vez de criar duas classes para associar Features:Personalize
e Features:WeatherStation
, a seguinte classe é usada para cada seção:
public class Features
{
public const string Personalize = nameof(Personalize);
public const string WeatherStation = nameof(WeatherStation);
public bool Enabled { get; set; }
public string ApiKey { get; set; }
}
O código a seguir configura as opções nomeadas:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<Features>(
Features.Personalize,
builder.Configuration.GetSection("Features:Personalize"));
builder.Services.Configure<Features>(
Features.WeatherStation,
builder.Configuration.GetSection("Features:WeatherStation"));
O código a seguir mostra as opções nomeadas:
public sealed class Service
{
private readonly Features _personalizeFeature;
private readonly Features _weatherStationFeature;
public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
{
_personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
_weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
}
}
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.
Usar os serviços de injeção de dependência para configurar as opções
Ao configurar opções, é possível usar a injeção de dependência para acessar serviços registrados e usar isso para configurar opções. Isso é útil quando for necessário acessar serviços para configurar opções. Os serviços podem ser acessados da DI ao configurar as opções de duas maneiras:
Passar um delegado de configuração para Configurar em OptionsBuilder<TOptions>.
OptionsBuilder<TOptions>
oferece sobrecargas de Configurar que permitem usar até cinco serviços para configurar opções:builder.Services .AddOptions<MyOptions>("optionalName") .Configure<ExampleService, ScopedService, MonitorService>( (options, es, ss, ms) => options.Property = DoSomethingWith(es, ss, ms));
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 Configurar, 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 Configurar. 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.
Usando o seguinte arquivo appsettings.json:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from Awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
A classe a seguir associa-se à seção de configuração "MyCustomSettingsSection"
e aplica algumas regras de DataAnnotations
:
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int Scale { get; set; }
[Required]
public required int VerbosityLevel { get; set; }
}
Na classe anterior SettingsOptions
, a propriedade ConfigurationSectionName
contém o nome da seção de configuração à qual associar. Nesse cenário, o objeto de opções fornece o nome da sua seção de configuração.
Dica
O nome da seção de configuração é independente do objeto de configuração ao qual ele está associando. Em outras palavras, uma seção de configuração nomeada "FooBarOptions"
pode ser associada a um objeto de opções chamado ZedOptions
. Embora possa ser comum nomeá-los da mesma forma, isso não é necessário e pode realmente causar conflitos de nome.
O seguinte código:
- Chamadas AddOptions para obter um OptionsBuilder<TOptions> que se associa à classe
SettingsOptions
. - Chamadas ValidateDataAnnotations para habilitar a validação usando
DataAnnotations
.
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations();
O método de extensão ValidateDataAnnotations
é definido no pacote Microsoft.Extensions.Options.DataAnnotations do NuGet.
O seguinte código exibe os valores de configuração ou relata erros de validação:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ValidationService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<SettingsOptions> _config;
public ValidationService(
ILogger<ValidationService> logger,
IOptions<SettingsOptions> config)
{
_config = config;
_logger = logger;
try
{
SettingsOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures)
{
_logger.LogError("Validation error: {FailureMessage}", failure);
}
}
}
}
O código a seguir aplica uma regra de validação mais complexa usando um representante:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
A validação ocorre em tempo de execução, mas você pode configurá-la para ocorrer na inicialização encadeando uma chamada para ValidateOnStart
:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.")
.ValidateOnStart();
A partir do .NET 8, você pode usar uma API alternativa, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), que permite a validação no início para um tipo de opções específico:
builder.Services
.AddOptionsWithValidateOnStart<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
IValidateOptions
para validação complexa
A classe a seguir implementa IValidateOptions<TOptions>:
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
sealed partial class ValidateSettingsOptions(
IConfiguration config)
: IValidateOptions<SettingsOptions>
{
public SettingsOptions? Settings { get; private set; } =
config.GetSection(SettingsOptions.ConfigurationSectionName)
.Get<SettingsOptions>();
public ValidateOptionsResult Validate(string? name, SettingsOptions options)
{
StringBuilder? failure = null;
if (!ValidationRegex().IsMatch(options.SiteTitle))
{
(failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
}
if (options.Scale is < 0 or > 1_000)
{
(failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
}
if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
{
(failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
}
return failure is not null
? ValidateOptionsResult.Fail(failure.ToString())
: ValidateOptionsResult.Success;
}
[GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
private static partial Regex ValidationRegex();
}
IValidateOptions
permite mover o código de validação para uma classe.
Observação
Este exemplo de código depende do pacote Microsoft.Extensions.Configuration.Json do NuGet.
Usando o código anterior, a validação está habilitada ao configurar os serviços com o seguinte código:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<SettingsOptions>(
builder.Configuration.GetSection(
SettingsOptions.ConfigurationSectionName));
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton
<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());
Pós-configuração de opções
Defina a pós-configuração com IPostConfigureOptions<TOptions>. A pós-configuração é executada depois que todas as configurações IConfigureOptions<TOptions> ocorrem e pode ser útil em cenários em que é necessário substituir a configuração:
builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
O PostConfigure está disponível para pós-configurar opções nomeadas:
builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Use PostConfigureAll para pós-configurar todas as instâncias de configuração:
builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});