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 adere a dois princípios importantes de engenharia de software:
- O Princípio de Segregação de Interface (ISP) ou Encapsulamento: Os cenários (classes) que dependem das definições de configuração dependem apenas das definições de configuração que utilizam.
- Separação de preocupações: as configurações para diferentes partes do aplicativo não são dependentes ou acopladas umas às outras.
As opções também fornecem um mecanismo para validar dados de configuração. Para obter mais informações, consulte a seção Validação de opções.
A maneira preferida de ler os valores de configuração relacionados é usando o padrão de opções. O padrão de opções é possível através da IOptions<TOptions> interface, onde o parâmetro TOptions
de tipo genérico é restrito a um class
. O IOptions<TOptions>
pode mais tarde ser fornecido através 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 realçados 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 TransientFaultHandlingOptions
classe:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
Ao usar o padrão options, uma classe options:
- Deve ser não-abstrato com um construtor público sem parâmetros
- Contêm propriedades públicas de leitura-gravação para vincular (os campos não são vinculados)
O código a seguir faz parte do arquivo C# Program.cs e:
- Chama ConfigurationBinder.Bind para vincular a
TransientFaultHandlingOptions
classe à"TransientFaultHandlingOptions"
seção. - 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 "TransientFaultHandlingOptions"
seção vinculada à TransientFaultHandlingOptions
instância. Isso hidrata as propriedades dos objetos C# com os valores correspondentes da configuração.
ConfigurationBinder.Get<T>
Vincula 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 TransientFaultHandlingOptions
classe:
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 com seus valores de propriedade preenchidos TransientFaultHandlingOptions
a partir da configuração subjacente.
Importante
A ConfigurationBinder classe expõe várias APIs, como .Bind(object instance)
e .Get<T>()
que não estão restritas a class
. Ao usar qualquer uma das interfaces Opções, você deve aderir às restrições de classe de opções acima mencionadas.
Uma abordagem alternativa ao usar o padrão de opções é vincular a "TransientFaultHandlingOptions"
seção e adicioná-la ao contêiner do serviço de injeção de dependência. No código a seguir, TransientFaultHandlingOptions
é adicionado ao contêiner de serviço com Configure e vinculado à configuração:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<TransientFaultHandlingOptions>(
builder.Configuration.GetSection(
key: nameof(TransientFaultHandlingOptions)));
O builder
exemplo anterior é uma instância de HostApplicationBuilder.
Gorjeta
O key
parâmetro é o nome da seção de configuração a ser pesquisada. Não tem de corresponder ao nome do tipo que o representa. Por exemplo, você pode ter uma seção nomeada "FaultHandling"
e ela pode ser representada pela TransientFaultHandlingOptions
classe. Neste caso, você passaria "FaultHandling"
para a GetSection função. O nameof
operador é 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 o início 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 à medida que elas ocorrem e reagir de acordo.
- Não suporta:
- Leitura de dados de configuração após o início do aplicativo.
- Opções nomeadas
- Está registado como Singleton e pode ser injetado em qualquer vida útil.
- É útil em cenários em que as opções devem ser recalculadas em cada resolução de injeção, em tempos de vida específicos ou transitórios. Para obter mais informações, consulte Usar IOptionsSnapshot para ler dados atualizados.
- Está registrado como Scoped e, portanto, não pode ser injetado em um serviço Singleton .
- Suporta opções nomeadas
- É usado para recuperar opções e gerenciar notificações de opções para
TOptions
instâncias. - Está registado como Singleton e pode ser injetado em qualquer vida útil.
- Apoios:
- Notificações de alteração
- Opções nomeadas
- Configuração recarregável
- Invalidação de opções seletivas (IOptionsMonitorCache<TOptions>)
IOptionsFactory<TOptions> é responsável pela criação de novas instâncias de opções. Tem um único Create método. A implementação padrão leva todos os registrados IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> executa todas as configurações primeiro, seguido pela pós-configuração. Ele distingue entre IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> só chama a interface apropriada.
IOptionsMonitorCache<TOptions> é usado por para armazenar instâncias em IOptionsMonitor<TOptions> cache TOptions
. O IOptionsMonitorCache<TOptions> invalida instâncias de opções no monitor para que o valor seja recalculado (TryRemove). Os valores podem ser introduzidos manualmente com TryAdd. O Clear método é usado quando todas as instâncias nomeadas devem ser recriadas sob demanda.
IOptionsChangeTokenSource<TOptions> é usado para buscar o que controla IChangeToken as alterações na instância subjacente TOptions
. Para obter mais informações sobre primitivos de token de alteração, consulte Alterar notificações.
O uso de um tipo de wrapper genérico oferece a capacidade de dissociar o tempo de vida da opção do contêiner de injeção de dependência (DI). A IOptions<TOptions>.Value interface fornece uma camada de abstração, incluindo restrições genéricas, no seu tipo de opções. Isso proporciona os seguintes benefícios:
- A avaliação da instância de configuração é adiada
T
para o acesso de , em vez de IOptions<TOptions>.Valuequando ela é injetada. Isso é importante porque você pode consumir aT
opção de vários lugares e escolher a semântica vitalícia sem alterar nada sobreT
. - Ao registrar opções do tipo
T
, você não precisa registrar explicitamente oT
tipo. 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 DI com um tempo de vida específico. - Do ponto de vista da API, ele permite restrições sobre o tipo
T
(neste caso,T
é restrito a um tipo de referência).
Quando você usa IOptionsSnapshot<TOptions>o , as opções são calculadas uma vez por solicitação quando acessadas e são 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 suportam 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>o .
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
se liga contra:
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
No código anterior, o Configure<TOptions>
método é usado para registrar uma instância de configuração que TOptions
será vinculada e atualiza as opções quando a configuração é alterada.
O IOptionsMonitor
tipo suporta notificações de alteração e permite cenários em que seu aplicativo pode precisar responder às alterações de origem de configuração dinamicamente. Isso é útil quando você precisa reagir a alterações nos dados de configuração depois que o aplicativo é iniciado. As notificações de alteração só são suportadas para provedores de configuração baseados em sistema de arquivos, como os seguintes:
- Microsoft.Extensões.Configuração.Ini
- Microsoft.Extensões.Configuração.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 início do aplicativo são lidas.
Gorjeta
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 IOptionsMonitor<TOptions> interface nesses ambientes, defina a DOTNET_USE_POLLING_FILE_WATCHER
variável de ambiente para 1
ou true
sonde o sistema de arquivos em busca de alterações. O intervalo em que as alterações são pesquisadas é a cada quatro segundos e não é configurável.
Para obter mais informações sobre contêineres do Docker, consulte Containerize a .NET app.
Opções nomeadas:
- São úteis quando várias seções de configuração se ligam às mesmas propriedades.
- Diferenciam maiúsculas de minúsculas.
Considere o seguinte arquivo appsettings.json :
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
Em vez de criar duas classes para vincular 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 exibe 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. IConfigureOptions<TOptions> as instâncias são tratadas como direcionadas à Options.DefaultName
instância, que é string.Empty
. IConfigureNamedOptions<TOptions> também implementa IConfigureOptions<TOptions>. A implementação padrão do IOptionsFactory<TOptions> tem lógica para usar cada um adequadamente. 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 utilizar esta convenção.
OptionsBuilder<TOptions> é usado para configurar TOptions
instâncias. OptionsBuilder
simplifica a criação de opções nomeadas, pois é apenas um único parâmetro para a chamada inicial AddOptions<TOptions>(string optionsName)
, em vez de aparecer em todas as chamadas subsequentes. A validação de opções e as ConfigureOptions
sobrecargas que aceitam dependências de serviço só estão disponíveis via OptionsBuilder
.
OptionsBuilder
é usado na seção Validação de opções.
Ao configurar opções, você pode usar a injeção de dependência para acessar serviços registrados e usá-los para configurar opções. Isso é útil quando você precisa acessar serviços para configurar opções. Os serviços podem ser acessados a partir do DI enquanto configuram as opções de duas maneiras:
Passe um delegado de configuração para Configurar no OptionsBuilder<TOptions>.
OptionsBuilder<TOptions>
fornece sobrecargas de Configure que permitem o uso de 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));
Crie um tipo que implemente IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> e registre o tipo como um serviço.
É recomendável passar um delegado de configuração para o Configure, já que a criação de um serviço é mais complexa. Criar um tipo é equivalente ao que a estrutura faz ao chamar Configure. Chamar Configure registra um genérico IConfigureNamedOptions<TOptions>transitório , que tem um construtor que aceita os tipos de serviço genéricos especificados.
A validação de opções permite que os valores das opções sejam validados.
Considere o seguinte arquivo appsettings.json :
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from Awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
A classe a seguir se liga à "MyCustomSettingsSection"
seção de DataAnnotations
configuração e aplica algumas regras:
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 ConfigurationSectionName
propriedade contém o nome da seção de configuração à qual se vincular. Nesse cenário, o objeto options fornece o nome de sua seção de configuração.
Gorjeta
O nome da seção de configuração é independente do objeto de configuração ao qual está vinculado. Em outras palavras, uma seção de configuração chamada "FooBarOptions"
pode ser vinculada a um objeto de opções chamado ZedOptions
. Embora possa ser comum nomeá-los da mesma forma, não é necessário e pode realmente causar conflitos de nomes.
O seguinte código:
- Chamadas AddOptions para obter um OptionsBuilder<TOptions> que se liga à
SettingsOptions
classe. - Chamadas ValidateDataAnnotations para habilitar a validação usando
DataAnnotations
o .
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations();
O ValidateDataAnnotations
método de extensão é definido no pacote NuGet Microsoft.Extensions.Options.DataAnnotations.
O código a seguir 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 delegado:
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ção 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.");
A seguinte classe 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.
Nota
Este código de exemplo depende do pacote NuGet Microsoft.Extensions.Configuration.Json .
Usando o código anterior, a validação é habilitada ao configurar 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>());
Defina a pós-configuração com IPostConfigureOptions<TOptions>o . A pós-configuração é executada depois que toda a IConfigureOptions<TOptions> configuração ocorre e pode ser útil em cenários em que você precisa substituir a configuração:
builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
PostConfigure está disponível para opções nomeadas pós-configuração:
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";
});
Comentários do .NET
O .NET é um projeto código aberto. Selecione um link para fornecer comentários: