Introdução

Toda aplicação .NET precisa de configuração — connection strings, chaves de API, feature flags, URLs de serviços externos. O que muitos desenvolvedores não percebem é que o .NET 8+ possui um pipeline interno de configuração sofisticado, onde múltiplos provedores são carregados em uma ordem específica, e o último a registrar um valor vence. Entender essa ordem é a diferença entre uma aplicação que funciona “por sorte” e uma que funciona por design.

Neste artigo, vamos explorar como o WebApplication.CreateBuilder() monta o pipeline de configuração, a ordem exata dos Add... no ConfigurationBuilder, como secrets do Docker e Podman se integram ao .NET sem código adicional, e como IOptions<T>, IOptionsSnapshot<T> e IOptionsMonitor<T> reagem a mudanças de configuração em runtime. Se você já teve um bug causado por uma variável de ambiente que sobrescreveu seu appsettings.json — ou o contrário — este artigo vai esclarecer definitivamente o que acontece por baixo dos panos.

💡 Dica: Este artigo usa .NET 8+ como referência, mas o pipeline de configuração é fundamentalmente o mesmo desde o .NET 6. As novidades do .NET 8 são refinamentos, não mudanças estruturais.


O Que Acontece em WebApplication.CreateBuilder()

Quando você chama WebApplication.CreateBuilder(args), o .NET executa internamente uma sequência precisa de registros no ConfigurationBuilder. Essa é a ordem padrão dos providers de configuração:

1
2
3
4
5
6
7
8
9
var builder = WebApplication.CreateBuilder(args);

// Internamente, o .NET 8 registra os providers nesta ordem:
// 1. ChainedConfigurationSource  (configuração do host)
// 2. appsettings.json             (reloadOnChange: true)
// 3. appsettings.{Environment}.json (reloadOnChange: true)
// 4. User Secrets                 (apenas em Development)
// 5. Environment Variables
// 6. Command-line arguments

ℹ️ Informação: A regra é simples — o último provider a registrar um valor sobrescreve os anteriores. Por isso variáveis de ambiente vencem appsettings.json, e argumentos de linha de comando vencem tudo.

A Ordem Completa e o Porquê

OrdemProviderreloadOnChangeQuando é registrado
1ChainedConfigurationSourceConfiguração herdada do host (raro manipular)
2appsettings.json✅ SimSempre — valores padrão da aplicação
3appsettings.{Environment}.json✅ SimSempre — sobrescreve valores por ambiente
4User Secrets (secrets.json)❌ NãoApenas quando ASPNETCORE_ENVIRONMENT=Development
5Environment Variables❌ NãoSempre — variáveis do sistema/container
6Command-line arguments❌ NãoSempre — argumentos passados via CLI

Essa hierarquia é intencional: valores mais seguros e específicos têm prioridade. O appsettings.json contém defaults; o ambiente (Development, Staging, Production) refina; secrets protegem dados sensíveis em dev; e variáveis de ambiente são o padrão em containers.


appsettings.json e Ambientes

O appsettings.json é o ponto de partida — ele contém os valores padrão da aplicação. O arquivo de ambiente (appsettings.{Environment}.json) não precisa conter todas as chaves — apenas as que deseja sobrescrever:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// appsettings.json — valores padrão
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "Default": "Host=localhost;Database=meudb;Username=dev;Password=dev123"
  },
  "App": {
    "NomeExibicao": "Minha Aplicação",
    "MaxItensPorPagina": 50,
    "HabilitarCache": true
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// appsettings.Production.json — sobrescreve apenas o necessário
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "App": {
    "MaxItensPorPagina": 100,
    "HabilitarCache": true
  }
}

⚠️ Atenção: O appsettings.Production.json nunca deve conter connection strings ou secrets de produção. Esses valores devem vir de variáveis de ambiente (containers) ou key vaults (cloud). O arquivo de produção serve apenas para ajustar comportamentos como log level, paginação e flags.

Como o .NET Determina o Ambiente

A variável ASPNETCORE_ENVIRONMENT (ou DOTNET_ENVIRONMENT para aplicações non-web) define qual appsettings.{Environment}.json será carregado. Os valores convencionais são:

1
2
3
4
5
6
7
8
# Em desenvolvimento (padrão no Visual Studio e dotnet run)
ASPNETCORE_ENVIRONMENT=Development

# Em staging
ASPNETCORE_ENVIRONMENT=Staging

# Em produção
ASPNETCORE_ENVIRONMENT=Production
1
2
3
4
5
6
7
// O .NET usa IHostEnvironment para expor o ambiente atual
app.MapGet("/info", (IHostEnvironment env) => new
{
    Ambiente = env.EnvironmentName,       // "Development", "Production", etc.
    IsDev = env.IsDevelopment(),
    IsProd = env.IsProduction()
});

dotnet user-secrets — Protegendo Dados em Desenvolvimento

O mecanismo de User Secrets resolve um problema real: desenvolvedores que commitam appsettings.json com connection strings de banco, chaves de API ou credenciais. Os secrets são armazenados fora do repositório, no perfil do usuário do sistema operacional.

Como Funciona

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Inicializar secrets no projeto (cria UserSecretsId no .csproj)
dotnet user-secrets init --project src/MeuProjeto.API

# Adicionar um secret
dotnet user-secrets set "ConnectionStrings:Default" "Host=prod-server;Database=producao;Password=S3nh@F0rt3!" --project src/MeuProjeto.API

# Listar todos os secrets
dotnet user-secrets list --project src/MeuProjeto.API

# Remover um secret
dotnet user-secrets remove "ConnectionStrings:Default" --project src/MeuProjeto.API

Onde Ficam Armazenados

Sistema OperacionalCaminho
Windows%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json
Linux/macOS~/.microsoft/usersecrets/<UserSecretsId>/secrets.json

O UserSecretsId é um GUID gerado automaticamente no .csproj:

1
2
3
<PropertyGroup>
  <UserSecretsId>a1b2c3d4-e5f6-7890-abcd-ef1234567890</UserSecretsId>
</PropertyGroup>

💡 Dica: O secrets.json tem o mesmo formato do appsettings.json. Internamente, o .NET registra um JsonConfigurationProvider apontando para esse arquivo — por isso qualquer chave do appsettings.json pode ser sobrescrita via secret. Para entender como User Secrets se integram com EF Core Migrations em projetos multi-camada, veja EF Core Migrations: Multi-Projeto, Secrets e Scaffolding.

User Secrets NÃO Funcionam em Produção

O provider de User Secrets é registrado apenas quando o ambiente é Development:

1
2
3
4
5
// Código interno do WebApplicationBuilder (simplificado):
if (hostEnvironment.IsDevelopment())
{
    configBuilder.AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true);
}

Em produção, os valores devem vir de variáveis de ambiente (containers), key vaults (Azure Key Vault, AWS Secrets Manager) ou Docker/Podman secrets.


Variáveis de Ambiente — O Padrão em Containers

As variáveis de ambiente são o provider mais importante em ambientes containerizados. Elas sobrescrevem tanto appsettings.json quanto appsettings.{Environment}.json.

Convenção de Nomenclatura

O .NET converte o separador hierárquico : para __ (duplo underscore) em variáveis de ambiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// appsettings.json
{
  "ConnectionStrings": {
    "Default": "..."
  },
  "App": {
    "Email": {
      "SmtpHost": "smtp.mailtrap.io"
    }
  }
}
1
2
3
# Variável de ambiente equivalente (Linux/Docker/Podman)
ConnectionStrings__Default="Host=prod-server;Database=producao;Password=S3nh@F0rt3!"
App__Email__SmtpHost="smtp.sendgrid.net"
1
2
3
4
5
6
7
8
9
# docker-compose.yml / podman-compose.yml
services:
  api:
    image: minha-api:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__Default=Host=db;Database=producao;Password=${DB_PASSWORD}
      - App__Email__SmtpHost=smtp.sendgrid.net
      - App__MaxItensPorPagina=200

⚠️ Atenção: No Linux, variáveis de ambiente são case-sensitive. ConnectionStrings__Default é diferente de CONNECTIONSTRINGS__DEFAULT. O .NET trata ambas corretamente graças ao provider de Environment Variables, mas mantenha a convenção PascalCase com __ para consistência.

Prefixo para Filtragem

Em ambientes compartilhados, use prefixo para evitar colisões:

1
2
3
4
5
// Carregar apenas variáveis com prefixo MEUAPP_
builder.Configuration.AddEnvironmentVariables(prefix: "MEUAPP_");

// MEUAPP_ConnectionStrings__Default será mapeado para ConnectionStrings:Default
// (o prefixo é removido automaticamente)

Docker e Podman Secrets — A Abordagem Segura para Containers

Variáveis de ambiente resolvem muitos cenários, mas têm uma limitação de segurança: elas ficam visíveis via docker inspect, podman inspect, /proc/*/environ e logs de debug. Para dados verdadeiramente sensíveis (senhas de banco, chaves de API, certificados), Docker e Podman oferecem secrets — arquivos montados em memória (tmpfs) dentro do container.

Como Secrets Funcionam no Docker e Podman

Os secrets são armazenados no host e montados como arquivos somente leitura no container, em /run/secrets/<nome>:

1
2
3
4
5
# Docker — criar um secret
echo "S3nh@F0rt3!Pr0duc40" | docker secret create db_password -

# Docker Compose — definir secrets
echo "S3nh@F0rt3!Pr0duc40" > ./secrets/db_password.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# docker-compose.yml
services:
  api:
    image: minha-api:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    secrets:
      - db_password
      - api_key
    volumes:
      # alternativa: montar manualmente
      # - ./secrets:/run/secrets:ro

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
1
2
3
4
5
6
7
# Podman — criar um secret
echo "S3nh@F0rt3!Pr0duc40" | podman secret create db_password -

# Podman — usar o secret no container
podman run --secret db_password -e ASPNETCORE_ENVIRONMENT=Production minha-api:latest

# O secret fica disponível em /run/secrets/db_password dentro do container

Integrando Docker/Podman Secrets com .NET

O .NET não tem um provider nativo para /run/secrets/. A integração é feita via key-per-file provider — um provider de configuração que lê cada arquivo de um diretório como um par chave-valor, onde o nome do arquivo é a chave e o conteúdo é o valor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var builder = WebApplication.CreateBuilder(args);

// Registrar o provider de key-per-file para Docker/Podman secrets
var secretsPath = "/run/secrets";
if (Directory.Exists(secretsPath))
{
    builder.Configuration.AddKeyPerFile(
        directoryPath: secretsPath,
        optional: true,
        filePrefix: "" // sem prefixo — usa o nome do arquivo como chave
    );
}

Com essa configuração, um arquivo /run/secrets/ConnectionStrings__Default com o conteúdo Host=db;Database=prod;Password=S3nh@! será mapeado para Configuration["ConnectionStrings:Default"].

ℹ️ Informação: O AddKeyPerFile usa a convenção __: automaticamente, igual às variáveis de ambiente. Então um arquivo chamado App__Email__SmtpHost será acessível como Configuration["App:Email:SmtpHost"].

Convenção de Nomes para Secrets

Para manter a compatibilidade com a hierarquia do .NET:

Secret (nome do arquivo)Chave na ConfigurationConteúdo do arquivo
ConnectionStrings__DefaultConnectionStrings:DefaultHost=db;Database=prod;Password=S3nh@!
App__Email__SmtpHostApp:Email:SmtpHostsmtp.sendgrid.net
App__JwtSecretKeyApp:JwtSecretKeyminha-chave-jwt-super-secreta-256bits

Podman Secrets vs Docker Secrets

CaracterísticaDocker SecretsPodman Secrets
RequisitoDocker Swarm modeFunciona sem Swarm (rootless)
ArmazenamentoRaft log (criptografado)Diretório local (criptografável)
Montagem/run/secrets/ (tmpfs)/run/secrets/ (tmpfs)
Visível em inspect?❌ Não❌ Não
Visível em /proc?❌ Não❌ Não
CLIdocker secret createpodman secret create
Composedocker compose com secrets:podman-compose com secrets:

💡 Dica: Se sua aplicação roda em Podman rootless (como descrito no deploy deste blog), prefira Podman secrets com AddKeyPerFile. Eles não ficam expostos em logs, inspect ou variáveis de ambiente — uma camada de segurança extra sem complexidade adicional.


A Ordem Final Completa (com Docker Secrets)

Quando você adiciona todos os providers, incluindo Docker/Podman secrets, a ordem completa do pipeline fica:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var builder = WebApplication.CreateBuilder(args);

// Providers registrados automaticamente pelo .NET 8:
// 1. ChainedConfigurationSource
// 2. appsettings.json                  (reloadOnChange: true)
// 3. appsettings.{Environment}.json    (reloadOnChange: true)
// 4. User Secrets                      (apenas Development)
// 5. Environment Variables
// 6. Command-line arguments

// Provider adicional — Docker/Podman secrets (REGISTRAR APÓS os defaults):
if (Directory.Exists("/run/secrets"))
    builder.Configuration.AddKeyPerFile("/run/secrets", optional: true);

// Ordem final de prioridade (último vence):
// 1.  appsettings.json               ← menor prioridade
// 2.  appsettings.Production.json
// 3.  User Secrets (apenas Dev)
// 4.  Environment Variables
// 5.  Command-line args
// 6.  Docker/Podman Secrets           ← maior prioridade (se registrado por último)

Diagrama do pipeline de configuração do .NET 8+ mostrando a ordem de carregamento dos providers e a prioridade de sobrescrita entre appsettings, secrets, variáveis de ambiente e Docker secrets

⚠️ Atenção: A posição em que você chama AddKeyPerFile define sua prioridade. Se quiser que variáveis de ambiente tenham prioridade sobre Docker secrets, registre o AddKeyPerFile antes de AddEnvironmentVariables. A regra é sempre: o último registrado vence.


IOptions<T> e Suas Variantes

Com o pipeline montado, o próximo passo é consumir a configuração de forma tipada. O .NET oferece três interfaces no namespace Microsoft.Extensions.Options, e cada uma se comporta de forma diferente quanto ao ciclo de vida e ao recarregamento:

Registrando a Configuração Tipada

1
2
3
4
5
6
// Program.cs
builder.Services.Configure<AppConfig>(
    builder.Configuration.GetSection("App"));

builder.Services.Configure<EmailConfig>(
    builder.Configuration.GetSection("App:Email"));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// AppConfig.cs — classe POCO sem dependência do framework
public sealed class AppConfig
{
    public string NomeExibicao { get; set; } = string.Empty;
    public int MaxItensPorPagina { get; set; } = 50;
    public bool HabilitarCache { get; set; }
}

public sealed class EmailConfig
{
    public string SmtpHost { get; set; } = string.Empty;
    public int SmtpPort { get; set; } = 587;
    public string SmtpUsername { get; set; } = string.Empty;
}

IOptions<T> — Singleton, Sem Reload

1
2
3
4
5
6
public class MeuServico(IOptions<AppConfig> options)
{
    private readonly AppConfig _config = options.Value;

    public int ObterMaxItens() => _config.MaxItensPorPagina;
}
CaracterísticaComportamento
Ciclo de vidaSingleton (Value é calculado uma vez e cacheado para sempre)
Recarregamento❌ Não recarrega — se o appsettings.json mudar, Value continua com o valor original
Quando usarConfigurações que nunca mudam durante o ciclo de vida da aplicação
Registrationservices.Configure<T>(section)

IOptionsSnapshot<T> — Scoped, Reload por Request

1
2
3
4
5
6
public class MeuServico(IOptionsSnapshot<AppConfig> options)
{
    private readonly AppConfig _config = options.Value;

    public int ObterMaxItens() => _config.MaxItensPorPagina;
}
CaracterísticaComportamento
Ciclo de vidaScoped (novo valor a cada request HTTP / escopo de DI)
Recarregamento✅ Recarrega — se o appsettings.json mudar, o próximo request já vê o novo valor
Quando usarAPIs e web apps onde configurações podem mudar sem restart
Restrição⚠️ Não pode ser injetado em serviços Singleton
Registrationservices.Configure<T>(section) (mesmo registro)

IOptionsMonitor<T> — Singleton com Notificação

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MeuServico : IDisposable
{
    private readonly IDisposable? _changeListener;
    private AppConfig _config;

    public MeuServico(IOptionsMonitor<AppConfig> monitor)
    {
        _config = monitor.CurrentValue;

        // Receber notificação quando a configuração mudar
        _changeListener = monitor.OnChange(novaConfig =>
        {
            _config = novaConfig;
            Console.WriteLine($"Config alterada! MaxItens agora é {novaConfig.MaxItensPorPagina}");
        });
    }

    public int ObterMaxItens() => _config.MaxItensPorPagina;

    public void Dispose() => _changeListener?.Dispose();
}
CaracterísticaComportamento
Ciclo de vidaSingleton (pode ser injetado em qualquer serviço)
Recarregamento✅ Recarrega — CurrentValue sempre retorna o valor mais recente
NotificaçãoOnChange() permite reagir a mudanças em tempo real
Quando usarServiços Singleton que precisam reagir a mudanças de configuração
Registrationservices.Configure<T>(section) (mesmo registro)

Comparativo Rápido

InterfaceCiclo de VidaReloadNotificaçãoUsar em Singleton?
IOptions<T>Singleton
IOptionsSnapshot<T>Scoped✅ (por request)
IOptionsMonitor<T>Singleton✅ (em tempo real)OnChange()

Named Options — Múltiplas Instâncias da Mesma Configuração

Quando você tem múltiplas configurações do mesmo tipo (ex: vários provedores de email, múltiplos bancos de dados):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "EmailProviders": {
    "Transacional": {
      "SmtpHost": "smtp.sendgrid.net",
      "SmtpPort": 587
    },
    "Marketing": {
      "SmtpHost": "smtp.mailchimp.com",
      "SmtpPort": 587
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Registrar como Named Options
builder.Services.Configure<EmailConfig>("Transacional",
    builder.Configuration.GetSection("EmailProviders:Transacional"));
builder.Services.Configure<EmailConfig>("Marketing",
    builder.Configuration.GetSection("EmailProviders:Marketing"));

// Consumir via IOptionsSnapshot (ou IOptionsMonitor)
public class EmailService(IOptionsSnapshot<EmailConfig> options)
{
    public void EnviarTransacional(string para, string assunto)
    {
        var config = options.Get("Transacional");
        // usa config.SmtpHost, config.SmtpPort...
    }

    public void EnviarMarketing(string para, string assunto)
    {
        var config = options.Get("Marketing");
        // usa config.SmtpHost, config.SmtpPort...
    }
}

Validação de Configuração na Inicialização

Um erro comum é a aplicação iniciar com configuração inválida (connection string vazia, porta zerada) e só falhar quando o primeiro request chega. O .NET 8 permite validar na inicialização:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
builder.Services.AddOptions<AppConfig>()
    .Bind(builder.Configuration.GetSection("App"))
    .ValidateDataAnnotations()       // valida [Required], [Range], etc.
    .ValidateOnStart();              // falha IMEDIATAMENTE se inválido

builder.Services.AddOptions<EmailConfig>()
    .Bind(builder.Configuration.GetSection("App:Email"))
    .Validate(config =>
    {
        // Validação customizada
        return !string.IsNullOrWhiteSpace(config.SmtpHost)
            && config.SmtpPort > 0;
    }, "SmtpHost deve ser preenchido e SmtpPort deve ser maior que zero")
    .ValidateOnStart();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// AppConfig.cs com Data Annotations para validação (não para ORM!)
using System.ComponentModel.DataAnnotations;

public sealed class AppConfig
{
    [Required(ErrorMessage = "NomeExibicao é obrigatório")]
    public string NomeExibicao { get; set; } = string.Empty;

    [Range(1, 500, ErrorMessage = "MaxItensPorPagina deve estar entre 1 e 500")]
    public int MaxItensPorPagina { get; set; } = 50;

    public bool HabilitarCache { get; set; }
}

ℹ️ Informação: O ValidateOnStart() faz a aplicação falhar rapidamente se a configuração estiver inválida — em vez de falhar misteriosamente no primeiro request. Isso é especialmente importante em containers onde um startup rápido com erro claro é melhor que um container “rodando” que falha em cada request.


Quando o .NET Recarrega Configuração Automaticamente

Nem todos os providers suportam reload automático. Saber quais recarregam (e quais não) evita bugs sutis:

Providers com reloadOnChange: true

Os providers baseados em arquivo são os únicos que suportam recarregamento automático:

1
2
3
4
5
// O WebApplicationBuilder registra com reloadOnChange: true por padrão:
configBuilder.AddJsonFile("appsettings.json",
    optional: true, reloadOnChange: true);
configBuilder.AddJsonFile($"appsettings.{env.EnvironmentName}.json",
    optional: true, reloadOnChange: true);

Quando você edita o appsettings.json ou appsettings.Production.json enquanto a aplicação está rodando, o .NET:

  1. Detecta a mudança via FileSystemWatcher
  2. Recarrega o provider de configuração afetado
  3. Emite um token de mudança (IChangeToken)
  4. IOptionsSnapshot<T> passará a retornar o novo valor no próximo request
  5. IOptionsMonitor<T> dispara o callback OnChange() imediatamente
  6. IOptions<T> não é afetado — mantém o valor original

Providers que NÃO Recarregam

ProviderRecarrega?Motivo
appsettings.jsonreloadOnChange: true (padrão)
appsettings.{Env}.jsonreloadOnChange: true (padrão)
User Secrets (secrets.json)Registrado sem reloadOnChange
Environment VariablesVariáveis são lidas na inicialização e cacheadas
Command-line argsPassados uma vez na inicialização
AddKeyPerFile (Docker secrets)Lidos na inicialização (arquivo em tmpfs)

Forçando Reload com reloadOnChange

Se você precisa que outros arquivos JSON também recarreguem:

1
2
3
4
5
6
// Adicionar um arquivo JSON customizado COM reloadOnChange
builder.Configuration.AddJsonFile(
    path: "config/feature-flags.json",
    optional: true,
    reloadOnChange: true   // ← habilita o FileSystemWatcher
);

⚠️ Atenção: Em containers Docker/Podman, o reloadOnChange com FileSystemWatcher pode não funcionar dependendo do tipo de volume montado. Volumes do tipo bind mount geralmente funcionam, mas volumes nomeados (docker volume) ou tmpfs podem não emitir os eventos de filesystem necessários. Teste sempre no seu cenário específico.


Customizando o Pipeline de Configuração

Quando os providers padrão não atendem, você pode customizar o pipeline completamente:

Adicionando Providers Customizados

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var builder = WebApplication.CreateBuilder(args);

// Os providers padrão já foram registrados pelo CreateBuilder.
// Tudo que você adicionar abaixo terá MAIOR prioridade:

// Provider para Azure Key Vault
builder.Configuration.AddAzureKeyVault(
    new Uri("https://meu-vault.vault.azure.net/"),
    new DefaultAzureCredential());

// Provider para AWS Secrets Manager
builder.Configuration.AddSecretsManager(region: RegionEndpoint.SAEast1);

// Provider para HashiCorp Vault
builder.Configuration.AddVaultConfiguration(
    () => new VaultOptions { Address = "https://vault.meudominio.com" },
    "meuapp");

// Provider para Docker/Podman secrets
if (Directory.Exists("/run/secrets"))
    builder.Configuration.AddKeyPerFile("/run/secrets", optional: true);

Substituindo Completamente o Pipeline

Em cenários raros onde você precisa controlar 100% da ordem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // NÃO carregar appsettings.json automaticamente
    // (Cuidado: perde TODOS os providers padrão)
});

// Limpar e reconstruir manualmente
builder.Configuration.Sources.Clear();

builder.Configuration
    .AddJsonFile("config/base.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"config/base.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
    .AddEnvironmentVariables(prefix: "MEUAPP_")
    .AddCommandLine(args);

if (builder.Environment.IsDevelopment())
    builder.Configuration.AddUserSecrets<Program>();

if (Directory.Exists("/run/secrets"))
    builder.Configuration.AddKeyPerFile("/run/secrets", optional: true);

Dicas e Boas Práticas

  1. Nunca commite secrets em appsettings.json — use dotnet user-secrets em desenvolvimento e variáveis de ambiente ou Docker/Podman secrets em produção. Se um secret vazar no Git, considere-o comprometido imediatamente.

  2. Use ValidateOnStart() para toda configuração crítica — connection strings, chaves de API, URLs de serviços. Falhar na inicialização é sempre melhor que falhar no primeiro request às 3h da manhã.

  3. Prefira IOptionsMonitor<T> em serviços Singleton — se o serviço precisa reagir a mudanças de configuração. Use IOptionsSnapshot<T> apenas em serviços Scoped (controllers, services por request).

  4. Nomeie seus secrets de container com __ — use ConnectionStrings__Default como nome do arquivo de secret para manter compatibilidade com a hierarquia de configuração do .NET.

  5. Cuidado com reloadOnChange em containersFileSystemWatcher pode não funcionar com todos os tipos de volume. Teste o comportamento no seu ambiente antes de depender do reload automático.

  6. Use prefixo em variáveis de ambiente compartilhadasbuilder.Configuration.AddEnvironmentVariables(prefix: "MEUAPP_") evita colisões com variáveis do sistema e de outros serviços no mesmo host.

  7. Não misture configuração e código — a classe POCO de configuração deve ser simples (getters/setters). Lógica de negócio baseada em configuração deve ficar no serviço que consome o IOptions<T>, nunca na classe de configuração.

  8. Documente a ordem do seu pipeline — em projetos com muitos providers (Key Vault, Feature Flags, Docker secrets), documente explicitamente a ordem de prioridade para a equipe.


Conclusão

O pipeline de configuração do .NET 8+ é um sistema poderoso e extensível que segue uma regra simples: o último provider registrado vence. Entender essa ordem — de appsettings.json (menor prioridade) até Docker/Podman secrets ou line arguments (maior prioridade) — é fundamental para evitar bugs sutis de configuração que só aparecem em produção.

O trio IOptions<T>, IOptionsSnapshot<T> e IOptionsMonitor<T> cobre todos os cenários de consumo: configuração estática (Singleton), configuração por request (Scoped) e configuração reativa com notificação (Singleton com reload). A combinação com ValidateOnStart() garante que sua aplicação falhe rápido com uma mensagem clara, em vez de falhar misteriosamente em runtime.

Para containers Docker e Podman, o AddKeyPerFile("/run/secrets") é a ponte entre os secrets do container engine e o sistema de configuração do .NET — seguro, simples e sem dependência de pacotes externos. Isso se combina perfeitamente com a arquitetura de deploy com Podman rootless e secrets gerenciados via pipeline.

O próximo passo natural é garantir que suas entidades de domínio e acesso a dados estejam igualmente desacoplados — e para isso, o EF Core 8 com Fluent API é o complemento ideal deste artigo.


Leia Também


Referências