ASP.NET Configuration Manager

As configurações no ASP.NET 6 ganharam uma forma nova de leitura e carregamento com o modelo de hospedagem mínimo e o tipo ConfigurationManager. Neste artigo, você vê como ler valores do appsettings.json, separar ambientes, carregar blocos inteiros em objetos e sobrescrever tudo isso no Azure.

O que mudou nas configurações no ASP.NET 6

O .NET possui recursos de configuração bem interessantes, que permitem carregar informações definidas em arquivos JSON, variáveis de ambiente e linha de comando. Além disso, todas essas fontes chegam até a aplicação como um único conjunto de pares chave e valor.

No .NET 6, o template web passou a usar o WebApplication.CreateBuilder, e a propriedade builder.Configuration agora devolve um objeto do tipo ConfigurationManager. Esse tipo implementa ao mesmo tempo IConfigurationBuilder e IConfigurationRoot.

Na prática, isso significa que você adiciona novas fontes e lê valores no mesmo objeto, sem precisar chamar um Build intermediário. Por isso, as configurações no ASP.NET 6 ficam disponíveis desde as primeiras linhas do Program.cs.

Antes do .NET 6, era comum separar essa lógica entre a classe Program e a classe Startup. Com o novo modelo, todo o fluxo cabe em um único arquivo, o que simplifica bastante a leitura do código.

Os arquivos appsettings.json

Ao criar uma aplicação web, o template adiciona dois arquivos de configuração na raiz do projeto: o appsettings.json e o appsettings.Development.json. Normalmente, eles vêm com o seguinte conteúdo.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Importante: é possível alterar os nomes desses arquivos, porém isso não é recomendado. Neste exemplo, vamos manter os padrões.

O sistema de configuração lê o JSON e achata a hierarquia em chaves separadas por dois pontos. Assim, o valor Default dentro de Logging e LogLevel vira a chave Logging:LogLevel:Default.

Development e Production

Como você pode notar, existem dois arquivos diferentes, um com a palavra Development e outro sem. Isso acontece porque o ASP.NET trabalha com ambientes, definidos por uma variável chamada ASPNETCORE_ENVIRONMENT.

Durante o desenvolvimento, essa variável fica no arquivo launchSettings.json, dentro da pasta Properties. Esse arquivo controla as configurações de execução local, e tanto o Visual Studio quanto o Visual Studio Code e o Rider o utilizam para depurar a aplicação.

Quando nenhum ambiente é informado, o ASP.NET assume Production como padrão. Por isso, o arquivo appsettings.json funciona como base, enquanto o appsettings.Development.json sobrescreve apenas o que muda no ambiente de desenvolvimento.

Para demonstrar, vamos editar os dois arquivos e adicionar uma chave chamada Env, com um valor diferente em cada um.

appsettings.Development.json

{
  "Env": "development",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

appsettings.json

{
  "Env": "production",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Como ler configurações no ASP.NET 6

Você pode ler as configurações no ASP.NET 6 em dois momentos distintos: durante a construção da aplicação e depois que ela foi construída. Em ambos, a forma de acesso é a mesma.

// A aplicação está na fase de construção (build)
builder.Configuration.GetValue<string>("Env");

// A aplicação já está pronta para ser executada
app.Configuration.GetValue<string>("Env");

Importante: não confunda a construção da aplicação com a compilação do projeto. A compilação já aconteceu, e aqui estamos falando da execução do código no Program.cs.

O método GetValue permite definir o tipo de retorno de forma genérica. Neste caso, queremos converter o valor para string, por isso utilizamos GetValue<string>("Env").

Também é possível acessar o valor pelo indexador, como em builder.Configuration["Env"]. Contudo, o indexador sempre retorna texto, enquanto o GetValue converte para tipos como int, bool ou TimeSpan.

Para chaves aninhadas, basta usar os dois pontos como separador. Por exemplo, GetValue<string>("Logging:LogLevel:Default") retorna o nível de log padrão definido no arquivo.

Exibindo o valor em um endpoint

Para exemplificar, vamos exibir o valor no navegador usando o exemplo padrão de projeto web que o ASP.NET traz.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Retorna o valor da chave Env do arquivo de configuração
app.MapGet("/", () => app.Configuration.GetValue<string>("Env"));

app.Run();

Com isso, a rota raiz devolve o texto armazenado na chave Env. Dessa forma, fica fácil conferir qual arquivo a aplicação carregou em cada ambiente.

Testando as configurações no ASP.NET em cada ambiente

Para executar a aplicação em modo de desenvolvimento, você pode apertar o play na sua IDE favorita ou rodar o comando dotnet run. Em ambos os casos, o launchSettings.json define a variável ASPNETCORE_ENVIRONMENT como Development.

Para rodar a aplicação em modo Release, utilize a sequência abaixo, que compila, publica e executa o binário gerado.

# Compila e publica em modo Release
dotnet build -c Release
dotnet publish -c Release

# Executa o binário publicado
cd bin/Release/net6.0/publish
./YourProject.exe

O binário publicado não passa pelo launchSettings.json, então a variável de ambiente fica vazia e o ambiente padrão passa a ser Production. Assim, a rota raiz exibe production em vez de development.

Em outras palavras, o mesmo código mostra valores diferentes, cada um lido do seu respectivo arquivo. Esse comportamento é a base de todas as configurações no ASP.NET 6 que variam entre ambientes.

Lendo blocos de configurações no ASP.NET

Na seção anterior, você leu o valor de uma única chave. No entanto, existe uma forma mais simples de ler diversos valores de uma vez, utilizando o método GetSection.

O GetSection lê um bloco inteiro do arquivo de configuração e permite vinculá-lo a um objeto. Para isso, o binder compara as chaves do JSON com os nomes das propriedades da classe.

Vamos tomar como base a classe de configuração usada no Curso de Fundamentos do ASP.NET 6. Nela, existe um trecho dedicado às configurações de SMTP, usadas para envio de e-mails.

public class SmtpConfiguration
{
    public string Host { get; set; }
    public int Port { get; set; } = 25;
    public string UserName { get; set; }
    public string Password { get; set; }
}

Esse agrupamento deixa as configurações no ASP.NET mais organizadas, porque cada assunto ganha a sua própria seção no JSON.

Como o nome sugere, o GetSection obtém uma seção do arquivo. Então, precisamos estruturar o appsettings.Development.json para entregar essas informações corretamente.

{
  "SmtpConfiguration": {
    "Host": "smtp.sendgrid.net",
    "Port": "587",
    "UserName": "apikey",
    "Password": "your-password"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Note que usamos os mesmos nomes nas chaves do JSON e nas propriedades da classe SmtpConfiguration. Dessa forma, o sistema de configuração consegue inferir os valores e popular o objeto, inclusive convertendo o texto "587" para o inteiro da propriedade Port.

// Cria a instância e vincula a seção do arquivo a ela
var smtp = new SmtpConfiguration();
app.Configuration.GetSection("SmtpConfiguration").Bind(smtp);

Também vale saber que propriedades sem chave correspondente mantêm o valor padrão da classe. Por exemplo, sem a chave Port, a propriedade continuaria com o valor 25.

Se preferir uma linha só, o método Get<T> cria e preenche o objeto de uma vez, como em GetSection("SmtpConfiguration").Get<SmtpConfiguration>().

Usando o options pattern

Ler a seção manualmente funciona bem no Program.cs. Porém, quando outras classes precisam do mesmo objeto, o caminho recomendado é o options pattern.

Nesse modelo, você registra a seção no container de dependências e depois recebe o objeto já preenchido por meio de IOptions<T>.

// Registra a seção SmtpConfiguration no container de dependências
builder.Services.Configure<SmtpConfiguration>(
    builder.Configuration.GetSection("SmtpConfiguration"));

var app = builder.Build();

// Recebe as configurações já preenchidas via IOptions
app.MapGet("/smtp", (IOptions<SmtpConfiguration> options) =>
    options.Value.Host);

Com isso, as configurações no ASP.NET deixam de ser lidas em vários pontos do código e passam a chegar tipadas onde forem necessárias. Se quiser revisar como o container funciona, o artigo sobre injeção de dependência no ASP.NET Core explica o assunto em detalhes.

Connection strings nas configurações no ASP.NET

O arquivo de configuração possui uma seção especial dedicada às connection strings. Nela, você pode definir várias conexões e lê-las depois de forma simplificada.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost,1433;Database=balta;User ID=sa;Password=your-password"
  },
  "Env": "development",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Com a connection string armazenada, o método GetConnectionString procura o valor diretamente na seção ConnectionStrings.

// Lê a conexão DefaultConnection da seção ConnectionStrings
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

Guardar a conexão nas configurações no ASP.NET evita espalhar endereços de banco pelo código. Assim, trocar o servidor exige apenas a edição de um valor.

Na verdade, esse método é apenas um atalho para GetSection("ConnectionStrings")["DefaultConnection"]. Ainda assim, ele deixa o código mais legível e evita erros de digitação no nome da seção.

Protegendo dados sensíveis com user secrets

Senhas e connection strings no appsettings.json representam um risco real. Afinal, esse arquivo vai para o controle de versão junto com o projeto e fica disponível para todos que acessam o repositório.

Para o ambiente de desenvolvimento, o .NET oferece a ferramenta Secret Manager, acessada pelo comando dotnet user-secrets. Ela guarda os valores em um arquivo JSON no perfil do usuário, fora da pasta do projeto.

# Habilita user secrets no projeto (adiciona o UserSecretsId ao .csproj)
dotnet user-secrets init

# Armazena a connection string fora do repositório
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost,1433;Database=balta;User ID=sa;Password=your-password"

# Lista os segredos armazenados
dotnet user-secrets list

Os segredos ficam em %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json no Windows e em ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json no Linux e no macOS. Dessa forma, as configurações no ASP.NET continuam com a mesma estrutura, mas a senha nunca entra no repositório.

O código não muda em nada, porque o WebApplication.CreateBuilder já registra os user secrets quando o ambiente é Development. Além disso, esses valores têm prioridade sobre os arquivos appsettings, então basta remover a senha do JSON.

Vale lembrar que o Secret Manager não criptografa os valores. Portanto, ele serve apenas para desenvolvimento, e em produção o ideal é usar variáveis de ambiente, as configurações do Azure ou o Azure Key Vault.

A ordem de prioridade das configurações no ASP.NET

Entender quem sobrescreve quem evita muitas horas de depuração. Afinal, as configurações no ASP.NET vêm de várias fontes ao mesmo tempo.

Segundo a documentação oficial de configuração do ASP.NET Core, o CreateBuilder carrega as fontes nesta ordem, da maior para a menor prioridade:

  1. Argumentos de linha de comando.
  2. Variáveis de ambiente sem os prefixos ASPNETCORE_ ou DOTNET_.
  3. User secrets, apenas no ambiente Development.
  4. O arquivo appsettings.{Environment}.json.
  5. O arquivo appsettings.json.

Em resumo, a última fonte adicionada vence quando duas fontes definem a mesma chave. Por isso, uma variável de ambiente sempre substitui o valor do JSON, e é exatamente esse comportamento que o Azure aproveita.

Nas variáveis de ambiente, o separador de hierarquia é o duplo sublinhado. Assim, a variável SmtpConfiguration__Host sobrescreve a chave SmtpConfiguration:Host, porque nem todas as plataformas aceitam dois pontos no nome.

Para depurar, você pode listar todas as configurações no ASP.NET com app.Configuration.AsEnumerable(), de preferência apenas em builds de depuração. Assim, fica claro qual valor venceu em cada chave.

Você também pode adicionar fontes próprias, já que o ConfigurationManager aceita novos provedores a qualquer momento. Por exemplo, builder.Configuration.AddIniFile("appsettings.ini") inclui um arquivo INI com prioridade sobre as fontes anteriores.

Configurações no ASP.NET com Azure App Service

Embora exista um arquivo de configuração para produção, é comum armazenar as configurações da aplicação diretamente no servidor. Isso previne problemas de segurança e elimina o risco de enviar arquivos com valores errados.

O Azure App Service possui uma área de configuração justamente para isso. Ela armazena pares chave e valor, e qualquer item cadastrado ali substitui o valor do seu arquivo.

Então, se o seu arquivo tem uma chave chamada Env com o valor XPTO e você registrar a mesma chave no Azure com o valor Azure, o valor do Azure prevalece. Isso acontece porque o App Service entrega esses itens à aplicação como variáveis de ambiente.

Importante: para executar os comandos abaixo, você precisa do Azure CLI instalado e autenticado no seu terminal.

Criando um item

Para criar uma chave em um web app no Azure, utilize o comando az webapp config appsettings set, seguido dos parâmetros necessários.

az webapp config appsettings set --name <app-name> --resource-group <resource-group> --settings Env=Azure

Também é possível enviar várias chaves de uma vez, separando cada par por espaço no parâmetro --settings.

Listando os itens

Para listar todas as configurações armazenadas no web app, utilize o comando az webapp config appsettings list.

az webapp config appsettings list --name <app-name> --resource-group <resource-group> --output table

Esse comando também ajuda a salvar as configurações em um arquivo JSON antes de uma alteração em lote.

Excluindo um item

Para remover um item da configuração, utilize o comando az webapp config appsettings delete.

az webapp config appsettings delete --setting-names Env --name <app-name> --resource-group <resource-group> --output table

Depois da exclusão, a aplicação volta a usar o valor definido no appsettings.json. Em geral, o App Service reinicia o app quando as configurações mudam, então o novo valor entra em vigor em seguida.

Chaves hierárquicas no Azure

Para sobrescrever uma chave aninhada, use o mesmo formato delimitado no nome do item. No App Service para Windows, o separador é o dois pontos, enquanto no Linux o padrão é o duplo sublinhado.

# App Service no Linux
az webapp config appsettings set --name <app-name> --resource-group <resource-group> --settings SmtpConfiguration__Host=smtp.example.com

Com isso, as configurações no ASP.NET 6 em produção ficam totalmente fora do repositório, sem nenhuma alteração no código.

Connection strings no Azure

Assim como nos arquivos de configuração, o Azure possui uma área dedicada às connection strings, acessada pelo comando az webapp config connection-string.

Separar as conexões das demais configurações no ASP.NET também ajuda o Azure a informar o tipo de banco de cada uma.

O esquema é o mesmo das configurações, com os subcomandos list, set e delete para manipular as conexões armazenadas.

# Lista as connection strings do web app
az webapp config connection-string list --name <app-name> --resource-group <resource-group>

# Cria ou atualiza uma connection string do tipo SQL Azure
az webapp config connection-string set --name <app-name> --resource-group <resource-group> --connection-string-type SQLAzure --settings DefaultConnection="<connection-string>"

Na aplicação, o mesmo GetConnectionString("DefaultConnection") continua funcionando. Ou seja, o valor do Azure substitui o do arquivo sem que você precise mudar uma única linha.

Conclusão

Manipular as configurações no ASP.NET 6 é simples, porque o ConfigurationManager reúne arquivos, segredos, variáveis de ambiente e linha de comando em um único objeto. Além disso, o GetValue, o GetSection e o GetConnectionString cobrem a maior parte das necessidades do dia a dia.

Com a integração do Azure App Service, você ainda configura ambientes de produção de forma rápida e segura. Assim, o código permanece o mesmo e apenas os valores mudam de um ambiente para outro.

FAQ

Qual a diferença entre builder.Configuration e app.Configuration?

As duas propriedades acessam a mesma configuração. A primeira é usada durante a construção da aplicação, e a segunda depois da chamada ao builder.Build().

O que acontece se a chave não existir no arquivo?

O GetValue retorna o valor padrão do tipo, como null para string. Você também pode informar um valor padrão no segundo parâmetro do método.

Preciso reiniciar a aplicação após alterar o appsettings.json?

Não necessariamente, porque o template padrão recarrega os arquivos JSON quando eles mudam. Contudo, objetos já vinculados com Bind não se atualizam sozinhos.

User secrets funcionam em produção?

Não, o CreateBuilder só carrega os user secrets no ambiente Development. Em produção, use variáveis de ambiente, o Azure App Service ou o Azure Key Vault.

Por que o valor do Azure substitui o do appsettings.json?

O App Service entrega as configurações como variáveis de ambiente. Como essas variáveis têm prioridade maior que os arquivos JSON, o valor do Azure prevalece.