Configuração e o Options Pattern
Objective
Uma connection string digitada num arquivo C# funciona na sua máquina e falha em todo o resto. O ASP.NET Core separa o que o código precisa de de onde o valor vem: a configuração é uma visão única de chave-valor montada a partir de várias fontes, e o código lê dessa visão sem saber qual fonte forneceu cada valor. O objetivo é conhecer as fontes e a precedência entre elas, sobrescrever um valor por ambiente sem tocar no código, ligar uma seção a uma classe tipada com o options pattern e manter segredos fora do repositório.
Use Cases
- Tirar a connection string do código para que o mesmo build rode contra SQLite localmente e contra PostgreSQL em produção.
- Sobrescrever uma configuração num container com uma variável de ambiente, sem rebuild e sem editar arquivo.
- Falhar na inicialização quando uma configuração obrigatória está ausente, em vez de na primeira requisição que precisa dela.
- Manter uma chave de API fora do controle de versão durante o desenvolvimento local.
Deep Dive
Uma visão, vários providers
WebApplication.CreateBuilder monta um IConfiguration a partir destas fontes,
nesta ordem. Uma fonte posterior sobrescreve uma anterior para a mesma chave:
appsettings.jsonappsettings.{Environment}.json(por exemploappsettings.Development.json)- User secrets (só no ambiente
Development) - Variáveis de ambiente
- Argumentos de linha de comando
O nome do ambiente vem de ASPNETCORE_ENVIRONMENT (padrão Production). O
perfil do launchSettings.json o define como Development para o dotnet run
e a IDE, mas esse arquivo serve só ao desenvolvimento local e não é usado quando
a app é publicada.
Lendo um valor
As chaves são hierárquicas. Este JSON:
plaintext{ "ConnectionStrings": { "GameStore": "Data Source=GameStore.db" } }
é lido com um helper feito para a seção ConnectionStrings:
csharpvar connString = builder.Configuration.GetConnectionString("GameStore");
builder.Services.AddSqlite<GameStoreContext>(connString);O mesmo valor é endereçável como ConnectionStrings:GameStore, com dois-pontos
como separador. Esse separador é o motivo de variáveis de ambiente usarem outro.
Sobrescrevendo por ambiente
Variáveis de ambiente não podem conter dois-pontos em todo shell, então o separador nelas é um sublinhado duplo:
plaintextexport ConnectionStrings__GameStore="Data Source=GameStoreProd.db" dotnet run
Com essa variável definida, a app abre GameStoreProd.db e o código C# é
idêntico. Esse é o ponto: o código de configuração não sabe, e não deveria
saber, de onde veio um valor. Num container ou num host de nuvem você define a
mesma variável no deploy, sem arquivo para editar. A variável vive só na sessão
atual do shell, então fechar o terminal restaura o valor do JSON.
Settings tipados: o options pattern
Ler strings por chave espalha nomes mágicos pelo código. Ligue uma seção a uma classe e peça-a pelo tipo:
csharppublic sealed class StoreOptions
{
[Required] public string? Name { get; set; }
[Range(1, 100)] public int PageSize { get; set; } = 20;
}
builder.Services.AddOptions<StoreOptions>()
.BindConfiguration("Store")
.ValidateDataAnnotations()
.ValidateOnStart();
app.MapGet("/info", (IOptions<StoreOptions> options) => options.Value);Dois detalhes tornam isso melhor que Configuration["Store:PageSize"]. O valor
é um int tipado com padrão. E o ValidateOnStart roda as data annotations
quando a app inicia: com Name ausente, o host se recusa a subir com
OptionsValidationException: DataAnnotation validation failed for 'StoreOptions' members: 'Name' with the error: 'The Name field is required.', em vez de falhar
na primeira requisição que o lê.
Três interfaces, três ciclos de vida
IOptions<T>é singleton: lido uma vez, nunca atualizado. A escolha padrão.IOptionsSnapshot<T>é scoped: recalculado por requisição, então um arquivo alterado é visto na requisição seguinte. Não pode ser injetado num singleton.IOptionsMonitor<T>é singleton, sempre devolve o valor atual e pode notificar mudanças. Use em singletons e background services que precisam reagir a reloads.
Segredos
Tudo que é credencial não pertence ao appsettings.json, porque esse arquivo é
commitado. No desenvolvimento local, use o Secret Manager:
plaintextdotnet user-secrets init dotnet user-secrets set "Payments:ApiKey" "sk_test_..."
O valor fica no seu perfil de usuário, fora do repositório, e é carregado
automaticamente em Development. Em produção, use variáveis de ambiente ou um
cofre de segredos (Azure Key Vault, AWS Secrets Manager, Kubernetes Secrets).
Uma connection string do SQLite pode ficar no appsettings.json só porque não
carrega credenciais; uma de PostgreSQL com senha não pode.
Trade-offs
- Surpresas de precedência. Uma variável de ambiente esquecida na sua
máquina pode vencer o arquivo JSON em silêncio, e a app parece ignorar a sua
edição. Quando uma configuração "não pega", imprima
builder.Configuration.GetDebugView()para ver cada chave com o provider que a forneceu. - Tudo é string até ser ligado.
Configuration["Store:PageSize"]devolve uma string enullpara uma chave ausente, sem erro. Ligar a uma classe de options dá tipos eValidateOnStart.csharpvar size = builder.Configuration.GetValue<int>("Store:PageSize"); // 0 se ausente, sem aviso IOptions<T>nunca recarrega. Se você edita oappsettings.jsoncom a app rodando, oIOptions<T>mantém o valor antigo. EscolhaIOptionsMonitor<T>quando o reload importar, e aceite que uma mudança aplicada pela metade passa a ser possível.- Arquivos por ambiente se multiplicam.
appsettings.Staging.json,appsettings.Production.jsone variantes por cliente viram uma matriz. Deixe os arquivos JSON só com padrões e mova o que difere por deploy para variáveis de ambiente definidas pela plataforma. - Variáveis de ambiente são visíveis à árvore de processos. São melhores que um arquivo commitado, mas mais fracas que um arquivo de segredo montado ou um cofre para credenciais de alto valor, já que aparecem em listagens de processos e em crash dumps em algumas plataformas.