.NET Configuration

January 5, 2025 | 5 minutes read

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):

  1. App Settings (JSON Files):
    • appsettings.json
    • appsettings.{Environment}.json (e.g., appsettings.Development.json)
  2. User Secrets (in Development environment only)
  3. Environment Variables
  4. 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.

  1. 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}");
}
  1. 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);
  1. User Secrets CLI

To view secrets stored during development:

dotnet user-secrets list
  1. 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:

  1. Add a UserSecretsId to your .csproj file:
    <PropertyGroup>
        <UserSecretsId>your-unique-guid</UserSecretsId>
    </PropertyGroup>
    
  2. Initialize user secrets:
    dotnet user-secrets init
    
  3. Add a secret:
    dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Data Source=..."
    
  4. 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.

comments powered by Disqus

You may also like

.NET Configuration

If you’ve been working with .NET for a while, chances are you’ve used the …

Read More

Introduction to Microsoft Orleans

One of the frameworks in the .NET ecosystem that doesn’t get as much …

Read More