Configuration Sources

Where an ASP.NET Core app reads its settings from, and the order that decides which source wins.

Syntaxstring? value = builder.Configuration["Section:Key"];

An app needs values that should not be fixed in its code: a database address, a page size, a log level. Configuration is the .NET system that collects those values. Each value has a key and is held as a string. Each place values come from is a provider: a JSON file, the operating system environment, the command line.

How the sources stack

WebApplication.CreateBuilder(args) adds providers for you, in this order: appsettings.json, then appsettings.{Environment}.json, then user secrets when the environment is Development, then environment variables, then the command line. Providers are read in the order they were added. When two providers supply the same key, the one added later wins. So a command-line argument beats an environment variable, which beats a file.

Reading a value

Nested JSON is flattened into one key with a colon between the levels. The JSON {"Greeting":{"Text":"hi"}} becomes the key Greeting:Text. Read it with the indexer: builder.Configuration["Greeting:Text"]. GetSection("Greeting") returns the same data with that prefix applied, so section["Text"] is the same value. GetConnectionString("Default") is shorthand for builder.Configuration["ConnectionStrings:Default"]; the two are the same read.

What trips people up

Every value is a string, so "3" must be converted before you can add to it. A key that no provider supplied reads as null rather than throwing, so a misspelled key looks like an empty setting. Keys are compared ignoring case; values are not.

Example

Example · csharp
// Requires: dotnet add package Microsoft.Extensions.Configuration
using Microsoft.Extensions.Configuration;

// Two in-memory providers stand in for a file and an environment variable.
// They are read in the order they are added, so the second one wins.
IConfigurationRoot config = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["Greeting:Text"] = "hello from the base file",
        ["Greeting:Times"] = "1"
    })
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["Greeting:Times"] = "3"
    })
    .Build();

// The indexer takes the flattened key. A colon separates the levels.
Console.WriteLine($"text: {config["Greeting:Text"]}");
Console.WriteLine($"times: {config["Greeting:Times"]}");

// A section is the same data with the prefix already applied.
IConfigurationSection greeting = config.GetSection("Greeting");
Console.WriteLine($"same text: {greeting["Text"]}");

// A key nobody supplied is null, not an error.
Console.WriteLine($"missing: {greeting["Colour"] ?? "(null)"}");

// Output:
// text: hello from the base file
// times: 3
// same text: hello from the base file
// missing: (null)

When to use it

  • Point a locally running API at a shared staging database for one run, without editing any file.
  • Give a container the same image in staging and production and change only its page size and log level.
  • Work out why a deployed app is using the wrong database by checking which provider supplies that key last.

More examples

The two appsettings files

The environment-specific file is added after the plain one, so its keys replace the matching ones and nothing else. In Production, PageSize is 50 and MaxUploadBytes is still 1048576. Put the full set of keys in appsettings.json and only the differences in the per-environment file, so there is one place to look for what a setting means.

Example · json
// appsettings.json - checked in, loaded always
{
  "Limits": {
    "PageSize": 20,
    "MaxUploadBytes": 1048576
  },
  "ConnectionStrings": {
    "Default": "Host=localhost;Database=shop"
  }
}

// appsettings.Production.json - loaded on top when the environment is Production
{
  "Limits": {
    "PageSize": 50
  }
}

Adding a source of your own

Reach for this when a file is written by something other than your build, such as a mounted file in a container or an operator-edited override. Order is the whole point: calling AddJsonFile after CreateBuilder places it above the defaults, so it wins. If you need the opposite, there is no tidy way to insert below them, so load the file first with your own ConfigurationBuilder instead.

Example · csharp
var builder = WebApplication.CreateBuilder(args);

// Added after the default providers, so these values win over appsettings.json.
// reloadOnChange: true re-reads the file when it is edited on disk.
builder.Configuration.AddJsonFile("limits.json", optional: true, reloadOnChange: true);

var app = builder.Build();

app.MapGet("/limits", (IConfiguration config) => new
{
    pageSize = config["Limits:PageSize"],
    // Shorthand for config["ConnectionStrings:Default"] - the same read.
    connection = config.GetConnectionString("Default")
});

app.Run();

Overriding a setting from outside the app

This is how you change one value without editing a file or rebuilding, which is what you want when reproducing a problem or running a one-off job. The command line is the highest-priority default provider, so it is also the fastest way to confirm that a setting is being read at all: if passing it on the command line changes nothing, the app is not reading that key.

Example · bash
# An environment variable. Two underscores stand in for the colon.
export Limits__PageSize=50
dotnet run

# The command line is added last, so it beats the variable above.
# The bare -- separates dotnet's own arguments from the app's.
dotnet run -- --Limits:PageSize=100

# These forms are all accepted by the command-line provider:
#   --Limits:PageSize=100
#   --Limits:PageSize 100
#   /Limits:PageSize=100

Discussion

  • Be the first to comment on this lesson.