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:
appsettings.json(Base configuration)appsettings.{Environment}.json(Environment overrides)- User Secrets (Local development only)
- Environment variables
- 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.jsoncontains no production secrets or sensitive API keys. - Rely on Strongly Typed Classes: Do not inject
IConfigurationdirectly into consumers; maintain encapsulation usingIOptions<T>. - Use Delimiters Correctly: Ensure orchestration
scripts convert nested configurations to the double-underscore syntax
(
Section__SubSection__Key).