O Padrão de Opções em C#
[[academy-video-youtube({"vid": "ko1Ie9gDydY", "start_time": "0", "title": "The Options Pattern in C#", "creator": "Tim Corey", "length": "10m 15s"})]]
A configuração em aplicações .NET geralmente reside em appsettings.json, variáveis de ambiente ou segredos do usuário. Extrair esses dados de configuração de um arquivo e inseri-los nas suas classes de uma maneira limpa e testável é o que o padrão de opções resolve. Em vez de ler manualmente as chaves JSON ou passar strings brutas, você vincula uma seção de configuração a uma classe C# fortemente tipada e permite que a injeção de dependência a entregue onde for necessário.
Em seu vídeo "The Options Pattern in C#", Tim Corey introduz as três variações do padrão de opções (IOptions, IOptionsSnapshot, e IOptionsMonitor), demonstra cada uma em um aplicativo de servidor Blazor, e mostra como adicionar validação para que sua aplicação falhe rapidamente quando falta configuração ou está malformada. Se você está construindo qualquer projeto .NET que leia de arquivos de configuração e use injeção de dependência, este padrão é fundamental.
A Configuração: Um Modelo POCO e appsettings.json
[0:34 - 1:46] Tim começa com um aplicativo Blazor (renderizado no lado do servidor, sem interatividade no lado do cliente) que já tem duas partes em funcionamento. A primeira é uma seção em appsettings.json com três pares de chave-valor sob uma seção CloudInfo:
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}
A segunda peça é uma classe simples em C# (um POCO) cujas propriedades espelham as chaves JSON:
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}
Tim observa que a fonte de configuração não precisa ser appsettings.json especificamente. Pode ser appsettings.Development.json, secrets.json, ou qualquer combinação. O padrão de opções lê de qualquer provedor de configuração registrado no aplicativo, e os nomes das propriedades no POCO correspondem às chaves JSON por convenção.
Registrando Opções em Program.cs
[2:25 - 3:15] Conectar a configuração à injeção de dependência exige duas chamadas de método em Program.cs:
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");
AddOptions<t>() registra o tipo com DI. BindConfiguration("CloudInfo") informa ao framework qual seção da configuração mapear. A string "CloudInfo" corresponde à chave JSON em appsettings.json. Se o nome da seção e o nome da classe não corresponderem, o argumento de string é o que a estrutura usa para encontrar os dados corretos.
IOptions: A Abordagem Singleton
[3:15 - 5:18] Com o registro no lugar, Tim injeta a configuração em uma página Blazor usando IOptions<CloudInfoOptions>:
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}
O detalhe crítico é que IOptions<t> está registrado como um singleton. Os valores de configuração são lidos uma vez quando o aplicativo inicia e armazenados em cache pela vida útil do processo. Se você mudar appsettings.json enquanto o aplicativo está em execução, IOptions não refletirá essas mudanças.
Para a maioria dos aplicativos, essa é a escolha certa. A configuração raramente muda em tempo de execução, e o tempo de vida como singleton significa zero sobrecarga por solicitação.
IOptionsSnapshot: Recarregamento Controlado
[5:18 - 6:55] Tim troca IOptions por IOptionsSnapshot para demonstrar a variante com escopo:
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig
IOptionsSnapshot<t> lê a configuração novamente para cada solicitação (tempo de vida com escopo). Se você modificar appsettings.json enquanto o aplicativo está em execução, a próxima solicitação web captará os novos valores. A solicitação anterior mantém seu próprio snapshot, então não há inconsistência no meio da solicitação.
Isso é útil para aplicativos onde as mudanças de configuração precisam entrar em vigor sem reiniciar, como ativar/desativar flags de função, atualizar endpoints de API ou rodar strings de conexão de armazenamento. A troca é um pequeno custo por solicitação para reler a configuração, o qual é insignificante para a maioria das cargas de trabalho.
IOptionsMonitor: Notificações de Mudança ao Vivo
[6:55 - 8:34] A terceira variante, IOptionsMonitor<t>, vai além do recarregamento de instantâneos. Ele monitora ativamente as mudanças de configuração e pode disparar callbacks quando os valores são atualizados:
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}
Ao contrário de IOptionsSnapshot, que atualiza apenas entre solicitações, IOptionsMonitor pode detectar mudanças durante uma operação de longa duração. Tim aponta que isso é mais relevante para serviços de fundo, hubs SignalR ou qualquer componente com um tempo de vida mais longo que uma única solicitação HTTP.
Adicionando Validação
[8:34 - 10:05] The final piece Tim covers is validation. O padrão de opções suporta regras de validação inline que são executadas quando a configuração é acessada pela primeira vez:
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));
Tim demonstra o caso de falha removendo o valor Storage de appsettings.json e lançando o aplicativo. O resultado é uma exceção não tratada imediata: "CloudInfoOptions falhou na validação." O aplicativo se recusa a iniciar com a configuração incompleta, o que é exatamente o comportamento que você deseja. Descobrir um valor ausente na inicialização é muito melhor do que atingir uma referência nula em produção às 2 da manhã.
Para cenários de validação mais complexos (verificações entre propriedades, regras condicionais), você pode encadear múltiplas chamadas .Validate() ou implementar IValidateOptions<t> como uma classe de validação dedicada.
Concluindo: Três Interfaces, Um Padrão
[10:05 - 10:10] O padrão de opções fornece uma separação clara entre armazenamento e consumo de configuração. IOptions cobre a maioria dos casos de uso onde a configuração é estática. IOptionsSnapshot lida com aplicativos que precisam captar mudanças entre solicitações. IOptionsMonitor serve componentes de longa execução que necessitam de conhecimento em tempo real das atualizações de configuração. E a validação garante que seu aplicativo falhe ruidosamente quando valores obrigatórios estão ausentes.
Conclusão
[10:10 - 10:15] Para resumir: registre sua configuração com AddOptions<t>().BindConfiguration("SectionName") em Program.cs, injete uma das três interfaces (IOptions, IOptionsSnapshot, ou IOptionsMonitor) dependendo dos seus requisitos de recarregamento, e adicione .Validate() para capturar valores faltantes no início ao invés de em tempo de execução.
O padrão funciona em qualquer tipo de projeto .NET: Blazor, ASP.NET Core, aplicativos de console, serviços de trabalho. O registro é o mesmo em todos os lugares.
Dica de exemplo: Se você não tem certeza de qual interface usar, comece com IOptions<t>. É o mais simples, não tem sobrecarga por solicitação e cobre a vasta maioria dos casos onde a configuração é definida na inicialização e não muda. Só mude para IOptionsSnapshot ou IOptionsMonitor quando você tiver uma necessidade concreta de recarregamento em tempo de execução.
Assista o vídeo completo vídeo em seu Canal no YouTube e obtenha mais insights sobre padrões de configuração em C#.
