Configuration Providers and Precedence

Predict which value wins when one key is set in appsettings files, user secrets, environment variables and the command line.

Syntaxvar 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.

OrderSourceNotes
1ASPNETCORE_ and DOTNET_ variablesHost settings such as the environment name; prefix removed
2appsettings.jsonOptional, reloads on change
3appsettings.{Environment}.jsonOptional; the name must match exactly on Linux
4User secretsDevelopment only
5All environment variables__ becomes :
6Command-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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · csharp
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&lt;string, string&gt;</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.

Example · json
{
  // 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.

Example · bash
# 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

  • Be the first to comment on this lesson.