Most applications use different configurations for development, testing, staging, and production. A database connection used during development should not be the same as the one used in production. API endpoints, logging levels, authentication settings, and external service credentials may also change between environments.
Managing these differences becomes difficult when configuration values are copied manually or stored directly in application code.
For example, a developer may update an API endpoint in the development environment but forget to update the staging configuration. The application works locally but fails after deployment.
A better approach is to separate application configuration from application code, define clear environment-specific settings, and control how configuration changes reach each environment.
In this article, we will explore practical ways to manage configuration changes in ASP.NET Core applications across multiple environments.
Why Application Environments Need Separate Configuration
Consider an ASP.NET Core application that connects to a database, sends emails, and communicates with an external payment service.
Each environment may require different settings.
Configuration | Development | Staging | Production |
|---|
Database | Local database | Staging database | Production database |
Payment service | Sandbox | Sandbox | Live service |
Logging | Debug | Information | Warning |
Email service | Local testing | Test account | Production account |
Authentication | Development settings | Staging identity | Production identity |
The application code should remain the same across these environments. Only the configuration values should change.
This separation provides several benefits:
Developers can test without accessing production services.
Staging can reproduce production configuration more accurately.
Production credentials remain outside source code.
Configuration changes can be reviewed and deployed independently.
Deployment mistakes become easier to identify.
Understand ASP.NET Core Configuration Providers
ASP.NET Core supports multiple configuration providers. Each provider supplies configuration values to the application.
Common providers include:
JSON configuration files.
Environment variables.
Command-line arguments.
User Secrets for local development.
External configuration services.
Custom configuration providers.
By default, ASP.NET Core loads several configuration sources during application startup.
A typical configuration setup includes appsettings.json, an environment-specific JSON file, environment variables, and command-line arguments.
Later providers generally override earlier values when the same configuration key exists.
For example, an environment variable can override a value defined in appsettings.json without changing the file.
This makes it possible to keep common settings in source control while supplying environment-specific values during deployment.
Organize Configuration Files
A simple ASP.NET Core project may use the following structure:
MyApplication/
│
├── Controllers/
├── Services/
├── Models/
│
├── appsettings.json
├── appsettings.Development.json
├── appsettings.Staging.json
├── appsettings.Production.json
│
└── Program.cs
The main appsettings.json file should contain common settings that are safe to share across environments.
For example:
{
"Application": {
"Name": "OrderManagement",
"EnableDetailedErrors": false
},
"FileProcessing": {
"MaxFileSizeMb": 50,
"BatchSize": 500
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
The development configuration can override selected values:
{
"Application": {
"EnableDetailedErrors": true
},
"FileProcessing": {
"MaxFileSizeMb": 10
},
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}
The production configuration can use stricter settings:
{
"Application": {
"EnableDetailedErrors": false
},
"FileProcessing": {
"MaxFileSizeMb": 100
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
These examples show ordinary application settings. Production connection strings, API keys, passwords, and certificates should not be committed to configuration files in source control.
Also, environment-specific files should be used for non-secret settings rather than as a place to store production credentials.
Set the ASP.NET Core Environment
ASP.NET Core identifies the current environment using the ASPNETCORE_ENVIRONMENT environment variable.
Common environment names are:
Development
Staging
Production
You can check the current environment through IWebHostEnvironment.
var builder = WebApplication.CreateBuilder(args);
var environment = builder.Environment;
Console.WriteLine(
$"Current environment: {environment.EnvironmentName}");
builder.Services.AddControllers();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.MapControllers();
app.Run();
When the application runs with ASPNETCORE_ENVIRONMENT=Staging, ASP.NET Core loads appsettings.Staging.json if the file is available.
For production deployment, set the environment through the hosting platform or deployment configuration.
Do not assume that every hosting platform uses the same mechanism for environment variables. Check the hosting configuration before deploying.
Use Strongly Typed Configuration
Reading configuration values directly throughout the application makes maintenance difficult.
For example:
var batchSize = configuration[
"FileProcessing:BatchSize"];
This approach uses string keys and does not provide strong compile-time checking.
ASP.NET Core supports the Options pattern, which maps configuration sections to C# classes.
Create a configuration class:
public class FileProcessingOptions
{
public int MaxFileSizeMb { get; set; }
public int BatchSize { get; set; }
}
Register the configuration in Program.cs:
builder.Services
.AddOptions<FileProcessingOptions>()
.Bind(
builder.Configuration.GetSection(
"FileProcessing"))
.Validate(
options => options.MaxFileSizeMb > 0,
"Maximum file size must be greater than zero.")
.Validate(
options => options.BatchSize > 0,
"Batch size must be greater than zero.")
.ValidateOnStart();
The ValidateOnStart method ensures that invalid settings are detected during application startup rather than when a particular feature is first used.
Inject the options into a service:
public class FileImportService
{
private readonly FileProcessingOptions _options;
public FileImportService(
IOptions<FileProcessingOptions> options)
{
_options = options.Value;
}
public void DisplaySettings()
{
Console.WriteLine(
$"Maximum size: {_options.MaxFileSizeMb} MB");
Console.WriteLine(
$"Batch size: {_options.BatchSize}");
}
}
This approach keeps configuration access organized and makes it easier to validate application requirements.
Store Secrets Outside Application Configuration Files
Sensitive values should not be stored in source-controlled JSON files.
Examples include:
Database passwords.
Payment gateway API keys.
Email service credentials.
Cloud access keys.
Authentication signing keys.
For local development, ASP.NET Core User Secrets provides a convenient way to store development-only secrets outside the project directory.
Initialize User Secrets from the project directory:
dotnet user-secrets init
Add a development connection string:
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=OrderDb;Trusted_Connection=True;TrustServerCertificate=True"
The application can read the value through the normal configuration system:
var connectionString =
builder.Configuration.GetConnectionString(
"DefaultConnection");
User Secrets are intended for development. They are not an encrypted production secret-management system.
For production, use an appropriate secret manager, such as Azure Key Vault, AWS Secrets Manager, or another service supported by your hosting platform.
Apply least-privilege access so an application can retrieve only the secrets it needs.
Override Configuration Using Environment Variables
Environment variables are useful when deploying the same application package to multiple environments.
ASP.NET Core supports hierarchical configuration keys using double underscores.
For example, this configuration value:
{
"FileProcessing": {
"BatchSize": 500
}
}
Can be overridden using:
FileProcessing__BatchSize=1000
For a database connection string:
ConnectionStrings__DefaultConnection="Server=prod-server;Database=OrderDb;..."
This allows deployment pipelines or hosting platforms to supply environment-specific values without modifying the application files.
Avoid printing all environment variables or configuration values in application logs. Doing so can accidentally expose credentials.
Manage Configuration Changes During Deployment
Configuration management should be part of the deployment process.
A practical deployment workflow looks like this:
Developer Changes Configuration
|
v
Code Review
|
v
Automated Tests
|
v
Deploy to Staging
|
v
Validate Application Behavior
|
v
Approve Production
|
v
Deploy Production Settings
|
v
Monitor Application
The application package should be built once and promoted through environments whenever possible.
This reduces differences between the code tested in staging and the code running in production.
Configuration values should be supplied by each environment during deployment rather than manually editing published files.
For important configuration changes, keep a record of:
What changed.
Why the change was required.
Which environment received the change.
Who approved it.
When it was deployed.
How to roll back the change.
Handle Configuration Changes at Runtime
Some applications need configuration updates without restarting the application.
ASP.NET Core supports configuration reload for supported providers.
For example, a JSON provider can be configured to reload when the file changes:
builder.Configuration.AddJsonFile(
"customsettings.json",
optional: true,
reloadOnChange: true);
The Options pattern provides different ways to consume configuration updates.
Options interface | Behavior |
|---|
IOptions | Provides a configuration value that does not update during the application's lifetime |
IOptionsSnapshot | Provides a refreshed value for each scope |
IOptionsMonitor | Supports observing updated values and change notifications |
For example, IOptionsMonitor can be useful when a service needs to respond to a changed setting.
public class NotificationService
{
private readonly IOptionsMonitor<NotificationOptions>
_options;
public NotificationService(
IOptionsMonitor<NotificationOptions> options)
{
_options = options;
_options.OnChange(updatedOptions =>
{
Console.WriteLine(
"Notification configuration changed.");
});
}
}
Runtime reload behavior depends on the configuration provider. Not every external provider automatically reloads values in the same way.
More importantly, changing a configuration value does not guarantee that every dependent service can safely adopt it immediately.
For example, changing a database connection string may require connection-pool management or service recreation. Test runtime configuration updates before relying on them in production.
Common Configuration Management Mistakes
Committing production secrets to source control
Removing a secret from the latest file does not remove it from the repository's history. Rotate exposed credentials and use a proper secret manager.
Maintaining different application builds for each environment
Building separate packages for development, staging, and production increases the chance of deploying different code.
Build once and supply environment-specific configuration separately.
Using environment names inconsistently
A configuration file named appsettings.Production.json will not be loaded simply because a deployment is intended for production. The application environment must be configured correctly.
Allowing invalid settings to reach production
Missing connection strings, negative batch sizes, and invalid service URLs can cause failures after deployment.
Use startup validation for required configuration.
Updating configuration without a rollback plan
A configuration change can break an otherwise healthy application. Keep previous values or a versioned configuration history so a bad change can be reversed.
Exposing secrets through logs
Avoid logging complete connection strings, tokens, passwords, or configuration objects. Log the setting name or a safe status message instead.
Best Practices for Production Applications
Use the following practices to make configuration management more reliable:
Keep common non-secret settings in appsettings.json.
Use environment-specific files only for appropriate non-secret differences.
Store production credentials in a dedicated secret manager.
Use strongly typed options instead of scattered string-based configuration access.
Validate required settings during application startup.
Use environment variables or external configuration providers for deployment-specific values.
Promote the same application build through development, staging, and production.
Review and audit important configuration changes.
Test configuration updates before enabling runtime reload.
Monitor application behavior after configuration deployments.
Summary
Managing configuration across multiple environments is not just about maintaining separate JSON files. It requires a clear separation between application code, environment-specific settings, and sensitive credentials.
ASP.NET Core provides configuration providers, environment-specific files, environment variables, User Secrets, and the Options pattern to support this separation.
For production applications, combine strongly typed settings, startup validation, secure secret management, and a controlled deployment process. Keep a history of important changes and test configuration updates before releasing them.
A consistent configuration strategy makes deployments easier to manage and reduces the risk of environment-related failures.
Join the conversation! Your thoughts help the community grow.