43. Configuration Management
About this chapter
Manage configuration safely across environments by separating code from configuration, using appsettings hierarchy, user secrets, and cloud vaults for sensitive data.
- Configuration separation: Keeping secrets out of code
- appsettings hierarchy: Base, environment-specific, and override layers
- User Secrets: Safe local development configuration
- Environment variables: Overriding configuration in deployment
- Azure Key Vault: Production secret management
- Security best practices: Never hardcoding sensitive values
Learning outcomes:
- Understand configuration management principles
- Use appsettings files for different environments
- Configure User Secrets for local development
- Override configuration with environment variables
- Integrate Azure Key Vault for production secrets
- Implement configuration providers in dependency injection
43.1 Why Configuration Management Matters
Bad approach:
// ❌ WRONG - Hardcoded values
var connectionString = "Host=localhost;Database=commandapi;Username=postgres;Password=SuperSecret123!";
var apiKey = "sk_live_abc123xyz789";
var emailApiUrl = "https://api.sendgrid.com";
Problems:
- Security: Secrets in code visible to everyone
- Inflexibility: Different values per environment (dev, staging, prod)
- Accidental commits: Secrets pushed to GitHub (can’t be recovered!)
- Hard to update: Secrets change without code changes
Good approach:
Code Environment
(no secrets) (variables, vaults)
↓ ↓
Configuration
↓
Application
Configuration comes from safe sources, never from code.
43.2 The appsettings Hierarchy
Files and their purpose:
appsettings.json Base config (all environments)
appsettings.Development.json Dev-specific (checked in)
appsettings.Production.json Prod-specific (checked in)
User Secrets (development only) Sensitive dev values
Environment Variables Prod overrides
Azure Key Vault Production secrets
Precedence (top overwrites bottom):
Azure Key Vault
↓
Environment Variables
↓
appsettings.{ASPNETCORE_ENVIRONMENT}.json
↓
User Secrets (dev only)
↓
appsettings.json
43.3 appsettings.json Files
Base configuration (shared across environments):
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"AllowedHosts": "*",
"AppSettings": {
"ApiName": "CommandAPI",
"ApiVersion": "1.0.0",
"MaxPageSize": 50
},
"ConnectionStrings": {
"PostgreSqlConnection": "" // Empty—set per environment
}
}
Development-specific (checked into git):
// appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
}
},
"ConnectionStrings": {
"PostgreSqlConnection": "Host=localhost;Database=commandapi_dev;Username=postgres;Password=devpassword"
},
"AppSettings": {
"EnableDetailedErrors": true
}
}
Production-specific (checked into git, no secrets):
// appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ConnectionStrings": {
"PostgreSqlConnection": "" // Empty—set via env var or Key Vault
},
"AppSettings": {
"EnableDetailedErrors": false
}
}
What to commit:
✓ appsettings.json (base, no secrets)
✓ appsettings.Development.json (dev config, no real secrets)
✓ appsettings.Production.json (structure only, no secrets)
✗ appsettings.Production.Secrets.json (never commit!)
43.4 User Secrets for Development
User secrets store sensitive development values outside the codebase.
Initialize user secrets:
# In project directory
dotnet user-secrets init
# Creates: ~/.microsoft/usersecrets/{project-guid}/secrets.json
Set secrets:
# Set connection string
dotnet user-secrets set "ConnectionStrings:PostgreSqlConnection" \
"Host=localhost;Database=commandapi;Username=postgres;Password=realpassword"
# Set API key
dotnet user-secrets set "ApiKeys:SendGrid" "sg_abc123xyz"
# Set JWT secret
dotnet user-secrets set "JwtSecret" "very-secret-key-at-least-32-characters-long"
# List all secrets
dotnet user-secrets list
# Remove a secret
dotnet user-secrets remove "ApiKeys:SendGrid"
# Clear all secrets
dotnet user-secrets clear
User secrets in code:
// Program.cs - Automatically loaded in development
var builder = WebApplication.CreateBuilder(args);
// In development, user secrets are automatically loaded
// No additional configuration needed!
// They override appsettings.Development.json
var connectionString = builder.Configuration.GetConnectionString("PostgreSqlConnection");
// Gets value from user secrets if set, otherwise appsettings.Development.json
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(connectionString);
});
.gitignore user secrets:
# User secrets
**/secrets.json
**/secrets.*.json
Never commit secrets.json!
43.5 Environment Variables for Production
Environment variables override appsettings values.
Setting environment variables (Linux/Docker):
# Set variable
export ConnectionStrings__PostgreSqlConnection="Host=prod-server;Database=commandapi;..."
export ApiKeys__SendGrid="sg_abc123xyz"
export JwtSecret="very-secret-key"
# Run app
dotnet CommandAPI.dll
Docker Compose (environment):
services:
api:
image: commandapi:1.0
environment:
ASPNETCORE_ENVIRONMENT: Production
ConnectionStrings__PostgreSqlConnection: "Host=postgres;Database=commandapi;..."
ApiKeys__SendGrid: ${SENDGRID_API_KEY}
JwtSecret: ${JWT_SECRET}
Using .env file (Docker Compose):
# .env (add to .gitignore)
ASPNETCORE_ENVIRONMENT=Production
ConnectionStrings__PostgreSqlConnection=Host=prod-db;Database=commandapi;...
ApiKeys__SendGrid=sg_abc123xyz
JWT_SECRET=very-secret-key
Environment variable naming:
// JSON hierarchy uses colon separator in code
"ConnectionStrings": {
"PostgreSqlConnection": "..."
}
// Environment variables use double underscore (colon is invalid in some shells)
ConnectionStrings__PostgreSqlConnection=...
Access in code:
var builder = WebApplication.CreateBuilder(args);
// Access values (same code regardless of source)
var connectionString = builder.Configuration.GetConnectionString("PostgreSqlConnection");
var apiKey = builder.Configuration["ApiKeys:SendGrid"];
var jwtSecret = builder.Configuration["JwtSecret"];
43.6 Azure Key Vault Integration
For production, use Azure Key Vault for secrets.
Setup:
# Create key vault
az keyvault create --resource-group myresourcegroup --name mykeyvault
# Add secrets
az keyvault secret set --vault-name mykeyvault --name ConnectionStrings-PostgreSqlConnection \
--value "Host=prod-db;Database=commandapi;..."
az keyvault secret set --vault-name mykeyvault --name ApiKeys-SendGrid \
--value "sg_abc123xyz"
Add NuGet package:
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Identity
Program.cs configuration:
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
var builder = WebApplication.CreateBuilder(args);
// Load configuration from appsettings
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile(
$"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true,
reloadOnChange: true);
// In development, load user secrets
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
// In production, load from Azure Key Vault
if (builder.Environment.IsProduction())
{
var keyVaultUrl = new Uri(builder.Configuration["KeyVault:Url"]);
builder.Configuration.AddAzureKeyVault(
keyVaultUrl,
new DefaultAzureCredential()); // Uses managed identity in Azure
}
// Services
var connectionString = builder.Configuration.GetConnectionString("PostgreSqlConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(connectionString);
});
var apiKey = builder.Configuration["ApiKeys:SendGrid"];
builder.Services.AddSingleton<IEmailService>(
new SendGridEmailService(apiKey));
var app = builder.Build();
app.Run();
Managed Identity in Azure:
// ✓ CORRECT - Uses Azure managed identity (no secrets!)
var credential = new DefaultAzureCredential();
builder.Configuration.AddAzureKeyVault(
keyVaultUrl,
credential);
Local development with Key Vault:
// During development, if you want to test against real Key Vault:
if (builder.Environment.IsDevelopment())
{
var keyVaultUrl = new Uri(builder.Configuration["KeyVault:Url"]);
// Use DefaultAzureCredential with az login
builder.Configuration.AddAzureKeyVault(
keyVaultUrl,
new DefaultAzureCredential());
}
43.7 Configuration Classes for Type Safety
Strongly-typed configuration:
// Options/DatabaseOptions.cs
public class DatabaseOptions
{
public const string SectionName = "Database";
public string PostgreSqlConnection { get; set; }
public int CommandTimeout { get; set; } = 30;
public bool EnableDetailedErrors { get; set; }
}
// Options/ApiOptions.cs
public class ApiOptions
{
public const string SectionName = "Api";
public string ApiKey { get; set; }
public string JwtSecret { get; set; }
public int JwtExpiryMinutes { get; set; } = 60;
}
// appsettings.json
{
"Database": {
"PostgreSqlConnection": "...",
"CommandTimeout": 30,
"EnableDetailedErrors": false
},
"Api": {
"ApiKey": "",
"JwtSecret": "",
"JwtExpiryMinutes": 60
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
// Bind to options classes
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection(DatabaseOptions.SectionName));
builder.Services.Configure<ApiOptions>(
builder.Configuration.GetSection(ApiOptions.SectionName));
// Inject and use
builder.Services.AddScoped<CommandRepository>(provider =>
{
var dbOptions = provider.GetRequiredService<IOptions<DatabaseOptions>>();
return new CommandRepository(dbOptions.Value.PostgreSqlConnection);
});
var app = builder.Build();
// Access in controller
[ApiController]
[Route("api/[controller]")]
public class CommandsController : ControllerBase
{
private readonly IOptions<ApiOptions> _apiOptions;
public CommandsController(IOptions<ApiOptions> apiOptions)
{
_apiOptions = apiOptions;
}
[HttpPost]
public IActionResult Create(CommandMutateDto dto)
{
// Use configuration
var jwtSecret = _apiOptions.Value.JwtSecret;
// ...
}
}
43.8 Configuration Validation on Startup
Validate configuration before running:
// Options/ValidationExtensions.cs
public static class ConfigurationValidationExtensions
{
public static IServiceCollection ValidateOptions(this IServiceCollection services, IConfiguration configuration)
{
// Validate DatabaseOptions
var dbOptions = configuration.GetSection(DatabaseOptions.SectionName).Get<DatabaseOptions>();
if (string.IsNullOrWhiteSpace(dbOptions?.PostgreSqlConnection))
{
throw new InvalidOperationException(
"ConnectionString 'PostgreSqlConnection' is required and cannot be empty");
}
// Validate ApiOptions
var apiOptions = configuration.GetSection(ApiOptions.SectionName).Get<ApiOptions>();
if (string.IsNullOrWhiteSpace(apiOptions?.JwtSecret) || apiOptions.JwtSecret.Length < 32)
{
throw new InvalidOperationException(
"JwtSecret must be set and at least 32 characters long");
}
if (string.IsNullOrWhiteSpace(apiOptions?.ApiKey))
{
throw new InvalidOperationException(
"ApiKey is required");
}
return services;
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
// Validate on startup (fail fast)
builder.Services.ValidateOptions(builder.Configuration);
var app = builder.Build();
app.Run();
Data annotations validation:
using System.ComponentModel.DataAnnotations;
public class DatabaseOptions
{
public const string SectionName = "Database";
[Required]
public string PostgreSqlConnection { get; set; }
[Range(1, 300)]
public int CommandTimeout { get; set; } = 30;
}
// Validation with ValidateDataAnnotationsAttribute
services.Configure<DatabaseOptions>(
configuration.GetSection(DatabaseOptions.SectionName))
.ValidateDataAnnotations();
43.9 Configuration per Deployment Target
Development (user secrets + appsettings):
# User secrets set locally
dotnet user-secrets set "ConnectionStrings:PostgreSqlConnection" "..."
dotnet run
Docker local (environment in docker-compose):
services:
api:
build: .
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings__PostgreSqlConnection: "Host=postgres;..."
Production Azure (Key Vault + managed identity):
# Deploy app to Azure App Service
az webapp up --name commandapi --resource-group myresourcegroup
# Add Key Vault reference
az webapp config appsettings set \
--resource-group myresourcegroup \
--name commandapi \
--settings \
"KeyVault:Url=https://mykeyvault.vault.azure.net/"
# Assign managed identity
az webapp identity assign \
--resource-group myresourcegroup \
--name commandapi
# Grant Key Vault access to managed identity
az keyvault set-policy \
--name mykeyvault \
--object-id <managed-identity-id> \
--secret-permissions get list
43.10 Common Configuration Patterns
Feature flags (controlled rollout):
// appsettings.json
{
"FeatureFlags": {
"NewReportingEngine": false,
"BetaPagination": false,
"EnableNotifications": true
}
}
public class CommandService
{
private readonly IConfiguration _config;
public CommandService(IConfiguration config)
{
_config = config;
}
public async Task ProcessCommandAsync(Command command)
{
var useNewEngine = _config.GetValue<bool>("FeatureFlags:NewReportingEngine");
if (useNewEngine)
{
await NewReportingEngine.ProcessAsync(command);
}
else
{
await OldReportingEngine.ProcessAsync(command);
}
}
}
Logging configuration:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Information"
},
"ApplicationInsights": {
"InstrumentationKey": "..."
}
}
}
43.11 Security Checklist
Before shipping to production:
- ✓ No secrets in appsettings.json
- ✓ No secrets in appsettings.{Environment}.json
- ✓ .gitignore includes secrets.json and *.secrets.json
- ✓ User secrets not committed
- ✓ All secrets in Key Vault or environment variables
- ✓ Connection strings from Key Vault
- ✓ API keys from Key Vault
- ✓ JWT secrets from Key Vault
- ✓ Configuration validated on startup
- ✓ Error messages don’t expose secrets
- ✓ Managed identity used (not connection strings in config)
43.12 What’s Next
You now have:
- ✓ Understanding configuration hierarchy
- ✓ appsettings.json files for different environments
- ✓ User secrets for safe development
- ✓ Environment variables for production
- ✓ Azure Key Vault integration
- ✓ Type-safe configuration classes
- ✓ Configuration validation on startup
- ✓ Per-environment configuration strategies
- ✓ Feature flags and rollout control
- ✓ Security checklist
Next: CI/CD Pipeline—Automating builds, tests, and deployments with GitHub Actions.