Configuration Providers and Precedence
Predict which value wins when one key is set in appsettings files, user secrets, environment variables and the command line.
var limit = builder.Configuration.GetValue<int>("Orders:MaxItemsPerOrder", 50);
var orders = builder.Configuration.GetSection("Orders");
var shop = builder.Configuration.GetConnectionString("Shop");IConfiguration is a flat set of string keys and string values assembled from an ordered list of providers. Hierarchy is only a naming convention: the JSON {"Orders": {"MaxItemsPerOrder": 50}} becomes the key Orders:MaxItemsPerOrder. Keys are case-insensitive, values are always strings, and conversion to int or TimeSpan happens later, when you read or bind them.
The default sources and their order
WebApplication.CreateBuilder (and Host.CreateApplicationBuilder for workers) registers these sources. When two of them define the same key, the one added later wins.
| Order | Source | Notes |
|---|---|---|
| 1 | ASPNETCORE_ and DOTNET_ variables | Host settings such as the environment name; prefix removed |
| 2 | appsettings.json | Optional, reloads on change |
| 3 | appsettings.{Environment}.json | Optional; the name must match exactly on Linux |
| 4 | User secrets | Development only |
| 5 | All environment variables | __ becomes : |
| 6 | Command-line arguments | --Orders:Region=eu-west |
The .NET 10 builders also look for optional {ApplicationName}.settings.json and {ApplicationName}.settings.{Environment}.json right after the environment file; most projects never create them. Cast builder.Configuration to IConfigurationRoot and print Providers to see the real list for your app.
Overrides are per key, not per section
A later provider replaces individual keys. If appsettings.Staging.json sets only Orders:Currency, every other key under Orders still comes from appsettings.json. Arrays follow the same rule, because an array is just the keys :0, :1, :2. A one-element list in the environment file replaces element 0 and leaves elements 1 and 2 of the base file in place. That is how the example's Staging CORS policy keeps https://admin.localhost from the base file. Keep lists that differ per environment out of the base file, or key them by name so each entry is overridden explicitly.
Adding your own providers
builder.Configuration is a ConfigurationManager: a source you add is loaded immediately and goes to the end of the list. AddJsonFile("pricing.json") in Program.cs therefore beats environment variables and the command line, and operators lose the ability to override a pricing key at deploy time. If they must keep the last word, add environment variables and arguments again after your file.
What goes where
appsettings.json: non-secret defaults that are right everywhere.appsettings.{Environment}.json: non-secret differences such as log levels.- Environment variables and mounted secrets: per-deployment values and every secret.
- Command line: one-off overrides for a tool or a migration run.
Production failure modes
Every file source is optional, so mistakes are silent. appsettings.production.json loads on a developer's Mac and is skipped in the Linux container, because the file system there is case-sensitive and the environment is Production. A missing key reads as null and binds to the property's default. When a value looks wrong, ask the configuration which provider supplied it, as the example does, instead of guessing.
Example
When to use it
- Find out why staging still sends orders in USD by printing which provider supplied Orders:Currency at start-up.
- Raise Orders:MaxItemsPerOrder for one canary deployment with an environment variable, without rebuilding the image.
- Stop a production CORS policy from inheriting localhost origins from the base appsettings.json array.
More examples
Keeping the deployment in charge after adding a file
Sources are applied in the order they are added, so <code>AddJsonFile</code> after the builder's defaults puts the file at the top of the precedence list. Re-adding <code>AddEnvironmentVariables()</code> and <code>AddCommandLine(args)</code> after it means <code>Pricing__VatRate=0.21</code> on a container still overrides the file. The earlier environment provider stays in the list but is harmless: the later copy has the same values.
var builder = WebApplication.CreateBuilder(args);
// Appended after the defaults, so on their own these files would beat
// environment variables and command-line arguments.
builder.Configuration.AddJsonFile("pricing.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile($"pricing.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
// Adding these again restores the usual rule: the deployment has the final say.
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddCommandLine(args);
var app = builder.Build();
app.MapGet("/pricing/vat", (IConfiguration config) =>
TypedResults.Ok(new { vatRate = config.GetValue<decimal>("Pricing:VatRate") }));
app.Run();A base appsettings.json that is safe to commit
The base file documents every setting the service reads and gives each a safe default. Lists that change per environment are stored as objects, so <code>Cors:AllowedOrigins:Admin</code> can be overridden or set to an empty string on its own; binding the section to a <code>Dictionary<string, string></code> and taking the non-empty values gives the effective list. Secrets are deliberately absent, which makes a leaked image or repository far less damaging.
{
// Defaults that are right in every environment. This file is committed and
// copied into the image, so it never holds a secret.
"Logging": {
"LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }
},
"AllowedHosts": "*",
"Orders": {
"MaxItemsPerOrder": 50,
"Currency": "USD",
"ReservationTimeout": "00:15:00"
},
// Keyed by name instead of a JSON array: an environment file can override or
// blank one entry without inheriting the others by index.
"Cors": {
"AllowedOrigins": {
"Storefront": "https://shop.example.com",
"Admin": "https://admin.example.com"
}
}
// ConnectionStrings:Shop and Payments:ApiKey come from environment variables
// or the secret store, never from this file.
}Overriding from the shell
Everything after <code>--</code> is passed to the application rather than to <code>dotnet run</code>. The command-line provider accepts <code>--Key=value</code>, <code>--Key value</code> and <code>/Key value</code>. Because the builder reads host settings such as <code>environment</code> and <code>urls</code> from the same sources, an argument can switch which <code>appsettings.{Environment}.json</code> file loads, which is handy for reproducing a staging problem locally.
# Command-line arguments are the last provider, so they beat everything else.
dotnet run --project src/Orders.Api -- --Orders:Region=eu-west --Orders:MaxItemsPerOrder=100
# Environment variables beat the JSON files but lose to arguments.
Orders__Region=eu-west dotnet run --project src/Orders.Api
# --environment is host configuration, so it also selects appsettings.Staging.json.
dotnet src/Orders.Api/bin/Release/net10.0/Orders.Api.dll --environment Staging --urls http://0.0.0.0:8080
Discussion