Every deployed service needs environment-specific settings without code changes or committed secrets.
WebApplication.CreateBuilder loads configuration from several providers, later ones overriding earlier ones:
1. appsettings.json 2. appsettings.{Environment}.json (e.g. appsettings.Development.json) 3. User secrets (Development only, dotnet user-secrets set "Db:Password" "x") 4. Environment variables 5. Command-line arguments
Keys are hierarchical with : separators (ConnectionStrings:Main). In environment variables, use a double underscore instead: Smtp__Host=mail.local maps to Smtp:Host. This makes containers and cloud platforms easy to configure.
Read values directly with builder.Configuration["Smtp:Host"] or GetConnectionString("Main"), but prefer the options pattern: a plain class bound to a section.
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));Then inject IOptions<SmtpOptions> (read once, singleton), IOptionsSnapshot<T> (re-read per request, scoped) or IOptionsMonitor<T> (live updates with change notifications).
Validate options at startup so bad configuration fails fast: AddOptions<SmtpOptions>().BindConfiguration("Smtp").ValidateDataAnnotations().ValidateOnStart();.
Never commit secrets to appsettings.json. Use user secrets locally and environment variables, Azure Key Vault or another secret store in production.
Example
// appsettings.json: { "Smtp": { "Host": "localhost", "Port": 25 } }
// override in production with env vars: Smtp__Host=mail.example.com
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOptions<SmtpOptions>()
.BindConfiguration("Smtp")
.ValidateDataAnnotations()
.ValidateOnStart(); // fail fast on bad config
var app = builder.Build();
app.MapGet("/smtp", (IOptions<SmtpOptions> opts) => $"{opts.Value.Host}:{opts.Value.Port}");
app.MapGet("/raw", (IConfiguration config) => config["Smtp:Host"] ?? "unset");
app.Run();
// GET /smtp -> localhost:25 (or mail.example.com:25 with the env var)
public class SmtpOptions
{
[Required] public string Host { get; set; } = "";
[Range(1, 65535)] public int Port { get; set; } = 25;
}Remember
- Later providers override earlier ones: JSON, env-specific JSON, secrets, env vars, command line
- Use __ in environment variable names for nested keys
- Bind sections to classes with the options pattern
- Validate options on start; keep secrets out of source control
Watch out for
- Committing production passwords in appsettings.json.