Managing Configuration and Environments in C

Effective configuration management in C# separates application code from deployment-specific values, allowing seamless transitions across development, staging, and production environments. Modern .NET applications achieve this using a layered configuration system, hierarchical JSON files, environment variables, secure secret managers, and strongly typed settings via the Options pattern. This guide details how to implement robust, scalable, and secure environment-specific settings in C#.

The Layered Configuration Model

.NET uses an extensible, order-dependent configuration pipeline represented by the IConfiguration interface. When you configure the application host (typically in Program.cs), settings are merged sequentially from multiple providers. If keys conflict, the last loaded provider overwrites previous values.

A standard configuration pipeline prioritizes settings in the following order:

  1. appsettings.json (Base configuration)
  2. appsettings.{Environment}.json (Environment overrides)
  3. User Secrets (Local development only)
  4. Environment variables
  5. Command-line arguments
var builder = WebApplication.CreateBuilder(args);

// The default Host automatically loads the providers in the order above.
var configuration = builder.Configuration;

Determining the Current Environment

The application runtime determines its context using the ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT environment variable. Standard values include Development, Staging, and Production.

You can query the environment using IHostEnvironment:

if (builder.Environment.IsDevelopment())
{
    // Enable local debugging tools or mock services
}

When the runtime detects Development, it automatically attempts to load appsettings.Development.json after appsettings.json, overriding any matching keys defined in the base file.

Structured Configuration with JSON

Use appsettings.json for non-sensitive, shared defaults across all environments:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "Features": {
    "EnableExport": true
  }
}

Override specific values for production in appsettings.Production.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

Only keys specified in the environment-specific file are replaced; omitted keys retain values defined in appsettings.json.

Overriding via Environment Variables

Production deployments, container platforms (like Docker and Kubernetes), and CI/CD pipelines commonly rely on environment variables.

To override nested JSON settings using environment variables, use a double underscore (__) as a delimiter to represent hierarchy:

  • JSON key: "DatabaseSettings": { "ConnectionString": "..." }
  • Environment variable: DatabaseSettings__ConnectionString

This mechanism keeps sensitive values out of source control and enables dynamic injection at runtime.

The Options Pattern

Avoid accessing IConfiguration directly throughout your business logic. Instead, bind configuration sections to strongly typed classes using the Options pattern.

Define the settings model:

public class DatabaseOptions
{
    public const string SectionName = "Database";
    public string ConnectionString { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

Register the options in Program.cs:

builder.Services.Configure<DatabaseOptions>(
    builder.Configuration.GetSection(DatabaseOptions.SectionName));

Inject the settings into your services:

public class OrderService
{
    private readonly DatabaseOptions _options;

    public OrderService(IOptions<DatabaseOptions> options)
    {
        _options = options.Value;
    }
}

For applications requiring real-time configuration updates without restarting, use IOptionsSnapshot<T> (scoped lifetime) or IOptionsMonitor<T> (singleton lifetime) instead of IOptions<T>.

Handling Sensitive Data

Never store credentials, API keys, or connection strings in committed JSON files.

Local Development: Secret Manager

Use the .NET Secret Manager (User Secrets) during local development to store sensitive data outside the project directory.

Run these CLI commands in your project folder:

dotnet user-secrets init
dotnet user-secrets set "Database:ConnectionString" "Server=localhost;..."

These values are loaded automatically when builder.Environment.IsDevelopment() is true.

Production: External Key Vaults

In production environments, integrate dedicated secret managers such as Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault into the ConfigurationBuilder:

if (builder.Environment.IsProduction())
{
    builder.Configuration.AddAzureKeyVault(
        new Uri("https://your-vault.vault.azure.net/"),
        new DefaultAzureCredential());
}

Best Practices

  • Validate at Startup: Use AddOptions<TOptions>().ValidateDataAnnotations().ValidateOnStart() to fail fast if required settings are missing or invalid.
  • Keep Base Files Safe: Ensure appsettings.json contains no production secrets or sensitive API keys.
  • Rely on Strongly Typed Classes: Do not inject IConfiguration directly into consumers; maintain encapsulation using IOptions<T>.
  • Use Delimiters Correctly: Ensure orchestration scripts convert nested configurations to the double-underscore syntax (Section__SubSection__Key).