Updated on

A configuration provider is one source of settings. JSON files, environment variables, command-line arguments, a key vault, a database table: each is a provider, and each contributes key-value pairs to the single dictionary the application reads through IConfiguration.

A new ASP.NET Core application starts with four kinds of source already wired up, in a fixed order: appsettings.json, then appsettings.{Environment}.json, then user secrets in development, then environment variables, then command-line arguments.

Order is the whole mechanism. Providers run in the sequence they were added, and a key set by a later one replaces the same key set by an earlier one. That is why an environment variable overrides a file, and why a command-line argument overrides everything.

If configuration is failing before it ever reaches a provider, the previous article covers options validation.

To download the source code for this article, you can visit our GitHub repository. The source code for the whole series is here.

Let’s dive in.

What Is a Configuration Provider?

A configuration provider is a single source of configuration key-value pairs and the code that reads it. The JSON provider reads a file, the environment-variables provider reads the process environment, a custom provider reads whatever we teach it to.

Providers are chained, not chosen. An application can have one or eight, and every one of them writes into the same flat dictionary that IConfiguration exposes.

That is why the order matters more than the sources do. Providers run in the order they were added, and a key written by a later provider replaces the same key written by an earlier one, so the last provider in the chain always wins a conflict.

Nothing merges. A provider overwrites a key or leaves it alone; two providers contributing different keys in the same section both survive, and two providing the same key do not.

So which configuration providers are available to us?

Which Configuration Providers Does ASP.NET Core Offer?

ASP.NET Core ships providers for files, environment variables, command-line arguments, in-memory collections, per-file directories, Azure Key Vault and Azure App Configuration, plus a base to build a custom one on.

Four kinds of source are in the chain of a new project without any code: the JSON file provider reading two files, user secrets in development, environment variables, and the command line.

The rest are opt-in and each is one line in Program.cs. Most also need their own NuGet package, which is the practical difference between “available” and “already there”.

The file provider is really three. JSON, INI and XML all derive from the same base class, share the same optional and reloadOnChange arguments, and differ only in how they parse.

Reach past the defaults for a reason, not for variety. Sensitive values in production and settings shared across several deployed applications are the two reasons that come up.

ProviderReads fromIn the default chainHow to add
JSON file.json filesYes, appsettings.json and appsettings.{Environment}.jsonbuilder.Configuration.AddJsonFile("config.json", optional: true, reloadOnChange: true)
INI file.ini filesNobuilder.Configuration.AddIniFile("config.ini", optional: true, reloadOnChange: true)
XML file.xml filesNobuilder.Configuration.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
User secretsa JSON file outside the project folder, read by the JSON file provider. There is no separate user-secrets provider typeYes, in Development, and only when the project has a UserSecretsIddotnet user-secrets init, then builder.Configuration.AddUserSecrets<Program>()
Environment variablesprocess environment variablesYesbuilder.Configuration.AddEnvironmentVariables("MYAPP_")
Command lineargsYesbuilder.Configuration.AddCommandLine(args)
Memoryan in-memory collectionNobuilder.Configuration.AddInMemoryCollection(dictionary)
Key-per-fileone file per key in a directoryNobuilder.Configuration.AddKeyPerFile("/run/secrets", optional: true)
Azure Key Vaultan Azure Key VaultNobuilder.Configuration.AddAzureKeyVault(uri, credential), package Azure.Extensions.AspNetCore.Configuration.Secrets 1.5.2
Azure App Configurationan Azure App Configuration storeNobuilder.Configuration.AddAzureAppConfiguration(connectionString), package Microsoft.Extensions.Configuration.AzureAppConfiguration 8.6.0
CustomanythingNoan IConfigurationSource plus an IConfigurationProvider

There is no (path, reloadOnChange) overload on any of the three file formats, which is why asking for reloading means passing optional as well. Omit both and we get optional: false, reloadOnChange: false.

The custom configuration provider is a very powerful mechanism. It gives us the freedom to implement whichever configuration provider we want. Say, for example, we want to keep our configuration in a database table. No problem, a few lines of code, and we can add our custom provider to the application. When none of these fit, we can write our own.

A file configuration provider is by far the most used one, but there are some cases where we need to use others. It’s not uncommon for developers to work in big development teams on large projects, where even putting the sensitive information in the appsettings.json file can cause problems for others and commit rollbacks.

We’ve all been there. Environment variables and user secrets can help us with that. And when the value we want to change is a switch rather than a setting, feature flags are the mechanism built on top of this one.

There’s also the issue of production environment configuration values sensitivity. We just can’t use sensitive information in files in production. Azure Key Vault might be just the perfect solution for us in that case.

We’ve mentioned that configuration providers are executed in a specific order.

Let’s learn in which order they are executed in.

In What Order Does ASP.NET Core Configuration Load?

Providers run in the order they were added, and the last one to write a key wins. The default chain is fixed and worth memorising.

appsettings.json is read first, then appsettings.{Environment}.json, then user secrets when the environment is Development, then environment variables, then command-line arguments.

Read it backwards for precedence. A command-line argument beats an environment variable, which beats a user secret, which beats the environment file, which beats the base file. That ordering is not arbitrary: it runs from the source that is easiest to commit by accident to the one that is hardest.

Anything added in Program.cs lands after all of them and therefore wins by default. Adding a source is one call on builder.Configuration, and calling it repeatedly appends rather than replaces.

To start from nothing, builder.Configuration.Sources.Clear() empties the chain before anything is added back.

That chain is assembled by WebApplication.CreateBuilder(args) before a single line of our own code runs, which is why we can read a value from configuration during startup without setting anything up first. Projects written before .NET 6 built the same chain inside Host.CreateDefaultBuilder(args) and its ConfigureAppConfiguration() callback, which is the shape most of the older material shows.

We can use that knowledge to our benefit if we want to remove certain configuration providers or even reorder them, because builder.Configuration.Sources is an ordinary list. And adding sources is additive: every call appends to the end of the chain instead of replacing what is already there, so the environment-specific files loaded by the defaults keep working.

Adding a Simple ini Configuration File

We can demonstrate this by adding another configuration source, a simple ini file in the project root, appsettings.ini:

[Logging:LogLevel]
Default=Information
Microsoft=Warning
Microsoft.Hosting.Lifetime=Information

[ConnectionStrings]
sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True

On a web project targeting .NET 10 the INI provider already ships in the shared framework, so there is nothing to install; in a non-web project, add the Microsoft.Extensions.Configuration.Ini package first.

Now we can simply extend the chain in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
    .AddIniFile("appsettings.ini", optional: true, reloadOnChange: true)
    .AddIniFile($"appsettings.{builder.Environment.EnvironmentName}.ini", optional: true, reloadOnChange: true);

If we run the application now, we’ll see two more sources added to our existing ones.

But what if we want to use just a single source and not worry about overriding our values with different providers, like environment or command-line?

We can simply do that by clearing the sources before we add our provider, which is one extra line:

builder.Configuration.Sources.Clear();

builder.Configuration
    .AddIniFile("appsettings.ini", optional: true, reloadOnChange: true)
    .AddIniFile($"appsettings.{builder.Environment.EnvironmentName}.ini", optional: true, reloadOnChange: true);

Now if we run the application we’ll only see appsettings.ini and appsettings.{EnvironmentName}.ini as the sources of configuration:

single configuration provider

Awesome.

But, other configuration providers exist and we should not discard them, so let’s revert that Clear() line. Let’s see how we can use them. They might come in handy.

Using Default Configuration Providers

We’ve seen how to add a simple ini configuration file by using the file configuration provider. But we have three more ways to set the configuration values out-of-the-box.

User Secrets

User secrets are a convenient mechanism to store sensitive configuration data while in development. They are easy to use and you won’t have to create environment variables for each project you develop locally. It’s a good way to keep things clean and simple.

By default, the application uses secrets after the appsettings.json and appsettings.{Environment}.json files, and right before environment variables and command-line arguments. If you find yourself wondering why your secret isn’t working, it might be worthwhile to check your environment variables.

To be able to use secrets we can initialize the secret manager by navigating to the project root (ProjectConfigurationDemo) and typing:

dotnet user-secrets init

This will create a UserSecretsId element in our csproj file within the PropertyGroup element:

<PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <UserSecretsId>d5a08119-f24b-42c7-a729-38895bb7b067</UserSecretsId>
</PropertyGroup>

That element is what makes the provider appear at all. Without a UserSecretsId, no user-secrets source joins the chain even in the development environment.

Now we can set a user secret to set a connection string:

dotnet user-secrets set "ConnectionStrings:sqlConnection" "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"

Once we see the “Successfully saved … to the secret store”, we can check out all our secrets by typing:

dotnet user-secrets list

Or we can remove it by typing:

dotnet user-secrets remove "ConnectionStrings:sqlConnection"

We can also right-click on the project itself inside the Visual Studio and then go to the “Manage User Secrets” option to open the secrets.json file and check or modify our secrets:

{
    "ConnectionStrings:sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
}

Now if we run our application, our configuration values will be available at runtime.

There are more useful commands for user secrets, so if you’re interested, you can check out the documentation pages, and we cover the tooling around them in more depth in secret management in .NET.

Just remember, we should use user secrets just for the development environment. We should not use them in other environments. Choosing between a user secret and an environment variable once the code leaves our machine is a separate question, and we answer it in securing sensitive configuration data.

Environment Variables

We can do the same thing using environment variables. Environment variables although less convenient to use, are more appropriate for any environment, and therefore we can use them in production. However, they are unencrypted and if the machine gets compromised they are free for all to see.

The only thing we should be aware of is that when defining hierarchical configuration data we need to use double underscore __ instead of “:” as we do in a JSON file.

For example, we can define the connection string in cmd, where set takes a single NAME=value argument with no space and no quotes, because quotes would become part of the value:

set ConnectionStrings__sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True

Or in PowerShell, where the value is quoted:

$env:ConnectionStrings__sqlConnection = "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"

Both forms set the variable for the current window only, which is what a demonstration wants, since we run dotnet run in that same window. The setx command writes a persistent variable instead, but it is the wrong tool here: its variables are available in future command windows only, never in the one we are typing in.

And we can check if the variable is set by typing set in cmd, or Get-ChildItem Env: in PowerShell.

Command-line Arguments

We can use command-line arguments to set configuration values too. It’s not that common thing to do though. We can use it to test if certain values work correctly.

We can set the configuration via command-line in three different ways:

dotnet run "ConnectionStrings:sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
dotnet run "/ConnectionStrings:sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
dotnet run "--ConnectionStrings:sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"

The quotes are not decoration. A connection string contains semicolons, and every shell treats an unquoted semicolon as the end of the command, so the unquoted form silently passes a truncated value and then tries to run the rest as separate commands.

The three forms are equivalent, but do not mix the equals-sign form with the space-separated form in one command.

What Replaced ConfigureAppConfiguration in Minimal Hosting?

ConfigureAppConfiguration belongs to Host.CreateDefaultBuilder, the hosting model ASP.NET Core used before .NET 6. Projects created since then use WebApplication.CreateBuilder instead, and there is no ConfigureAppConfiguration call to make.

The replacement is more direct rather than different. builder.Configuration is the configuration builder, already populated with the default chain, so adding a source is one line:

builder.Configuration.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true);

Everything that worked inside the old callback works here. The environment is builder.Environment, sources appended land after the defaults, and builder.Configuration.Sources.Clear() still empties the chain.

Two things are genuinely gone. There is no separate host-configuration callback to distinguish from app configuration, and there is no ordering question about when the callback runs relative to the builder, because the lines run where they are written.

Old code keeps working. ConfigureAppConfiguration is not removed, and a project still using the generic host still uses it.

Minimal hosting changed more than the configuration call: the whole startup path of an ASP.NET Core Web API is different, and most of what is written about it online still assumes Startup.cs. The Ultimate ASP.NET Core Web API course builds a production API on the current model from the first commit.

Conclusion

In this article, we’ve learned what a configuration provider is, which providers ASP.NET Core offers, the order the default chain runs in, how to add a source of our own in Program.cs, and what replaced ConfigureAppConfiguration under minimal hosting. In the next part, we’re going to implement a custom configuration provider that reads our configuration values from the database.

You can find other parts of this series on the ASP.NET Core Web API page.

Tested with .NET 10.0.10.