If you’ve been working with .NET for a while, chances are you’ve used the configuration system to load settings from appsettings.json
, environment variables, or command-line arguments. It’s simple and convenient, but have you ever wondered how it works behind the scenes?
In this post, we’ll look at how .NET configuration works, how data is read from multiple sources, how the order of sources affects the final values, and how to keep your secrets safe both locally and in production.
This post is part of a series:
1..NET configuration
More follow soon
What is .NET configuration?
Configuration in .NET handles both settings and secrets such as API keys, database connection strings and feature flags. .NET’s modern configuration system that was introduced with .NET Core, replaced the older Web.config
format with a more flexible approach. The system supports multiple configuration formats and external providers.
Whether your configuration values are local (e.g., appsettings.json
) or remote (e.g., environment variables), the configuration system merges these sources into a single hierarchical configuration object accessible via IConfiguration
.
Building blocks of .NET configuration
The configuration system in .NET is composed of several building blocks that work together to load, manage, and access key-value pairs.
Configuration providers
Configuration providers are responsible for reading data from different sources and converting it into key-value pairs. Some common built-in providers include:
- JsonConfigurationProvider for JSON files like
appsettings.json
- EnvironmentVariablesConfigurationProvider for reading environment variables
- CommandLineConfigurationProvider for processing command-line arguments
- UserSecretsConfigurationProvider for reading values stored in a secrets.json file during local development
- InMemoryConfigurationProvider for reading in-memory key-value pairs for testing and programmatic configuration
Each provider implements the IConfigurationProvider
interface and parses its source data into hierarchical keys.
The provider takes the structure of the source (e.g., JSON, environment variables) and translates it into a key-value format. For example, given the following appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
The parsed keys are:
Logging:LogLevel:Default
→ “Information”Logging:LogLevel:Microsoft
→ “Warning”
Configuration sources
A configuration source defines where the data comes from. For example, a JSON configuration source points to appsettings.json
, while an environment variable source pulls values from system-level environment variables. Each source corresponds to a specific provider that reads the data.
Configuration Builder (IConfigurationBuilder)
The IConfigurationBuilder
constructs a configuration object by adding sources. You can chain multiple sources together, allowing the application to read settings from different locations:
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
var configuration = builder.Build();
In this example, the configuration reads from appsettings.json
and environment variables. Changes to appsettings.json
are automatically reloaded at runtime when reloadOnChange: true
is set.
Configuration Root (IConfigurationRoot)
The IConfigurationRoot
is the final configuration object after all sources are processed. It extends IConfiguration
and provides access to the merged configuration and offers the Reload()
method to refresh values dynamically.
Configuration Interface (IConfiguration)
IConfiguration
provides read-only access to configuration data. It allows retrieving values via keys:
string connectionString = configuration["ConnectionStrings:DefaultConnection"];
Or accessing sections using:
var logLevel = configuration.GetSection("Logging:LogLevel:Default").Value;
Custom Configuration Providers
You can create custom providers to load data from specialized sources, such as a database or an external API. Custom providers implement the IConfigurationProvider
interface and define how the data is read and exposed.
How are the sources read and combined?
Source priority
When using Host.CreateDefaultBuilder
or WebApplication.CreateBuilder
, the configuration system reads from multiple sources in the following order (lowest to highest priority):
- App Settings (JSON Files):
- appsettings.json
- appsettings.{Environment}.json (e.g., appsettings.Development.json)
- User Secrets (in Development environment only)
- Environment Variables
- Command-Line Arguments
If a key exists in multiple sources, the value from the higher-priority source overwrites the previous values.
Example
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=AppDb;"
}
}
Environment variable
ConnectionStrings__DefaultConnection="Server=prod;Database=ProdDb;"
The effective runtime value for DefaultConnection
becomes "Server=prod;Database=ProdDb;"
due to the higher priority of environment variables.
Customizing source order
You can customize the order of sources explicitly:
var configuration = new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables()
.AddJsonFile("customsettings.json", optional: true)
.Build();
In this example, command-line arguments have the highest priority.
Combining complex hierarchies
When merging sources, hierarchical sections are combined. For example:
appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Microsoft": "Information"
}
}
}
At runtime in the Development
environment, the effective configuration will be:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Information"
}
}
}
The settings from appsettings.Development.json
augment but do not overwrite the entire LogLevel object.
Debugging configuration issues
When dealing with complex configurations, debugging is essential.
- Log All Configuration Values
To log all configuration values at startup:
foreach (var kvp in configuration.AsEnumerable())
{
Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
- Debugging with GetDebugView()
The GetDebugView()
method (available in IConfigurationRoot
) displays the full configuration, including which provider supplied each value:
var debugView = ((IConfigurationRoot)configuration).GetDebugView();
Console.WriteLine(debugView);
- User Secrets CLI
To view secrets stored during development:
dotnet user-secrets list
- Middleware Debugging
For ASP.NET Core apps, you can add middleware to inspect configuration during HTTP requests:
app.Use(async (context, next) =>
{
var env = context.RequestServices.GetRequiredService<IConfiguration>().GetValue<string>("EnvironmentName");
await context.Response.WriteAsync($"Environment: {env}\n");
await next();
});
Managing Secrets Securely
User Secrets (Local Development)
During development, you might store sensitive data like API keys in user secrets to keep it out of source control.
Setting Up User Secrets:
- Add a
UserSecretsId
to your.csproj
file:<PropertyGroup> <UserSecretsId>your-unique-guid</UserSecretsId> </PropertyGroup>
- Initialize user secrets:
dotnet user-secrets init
- Add a secret:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Data Source=..."
- Access the secret in your app:
string dbConnection = configuration["ConnectionStrings:DefaultConnection"];
Azure Key Vault
In production environments, services like Azure Key Vault provide secure storage for secrets. It integrates seamlessly with .NET’s configuration system and supports features such as role-based access and integration with Azure Managed Identities. Detailed implementation and usage of Azure Key Vault will be covered in a future post.
Wrapping Up
The .NET configuration system is designed for flexibility and security. By understanding how different sources are combined, how priorities work, and how to handle secrets, you can build robust and secure applications. Whether using local files, environment variables, or cloud services, the configuration system adapts to your needs and supports secure development practices. Debugging tools like GetDebugView()
make it easier to identify and resolve issues, ensuring that your configuration remains transparent and reliable.
Take advantage of the built-in configuration providers and stay tuned for further posts on topics like Azure Key Vault and cloud-based configuration.