C# Configuration and Options Pattern
Configuration Overview
.NETβs configuration system provides a unified API for reading settings from multiple sources with support for hierarchical data, environment-specific overrides, and strongly-typed access.
// Configuration flows from multiple sources, later sources override earlier
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User secrets (Development only)
// 4. Environment variables
// 5. Command-line arguments
IConfiguration Basics
Reading Configuration Values
// Direct value access
string? connectionString = configuration["ConnectionStrings:DefaultDb"];
string? apiKey = configuration["ExternalServices:ApiKey"];
// GetValue with type conversion and default
int maxRetries = configuration.GetValue<int>("Settings:MaxRetries", 3);
bool enableFeature = configuration.GetValue<bool>("Features:NewDashboard");
// GetSection for hierarchical access
IConfigurationSection section = configuration.GetSection("Logging");
string? logLevel = section["LogLevel:Default"];
// Check if section exists
if (configuration.GetSection("OptionalFeature").Exists())
{
// Configure optional feature
}
Configuration Hierarchy
// appsettings.json
{
"ConnectionStrings": {
"DefaultDb": "Server=localhost;Database=MyApp",
"Redis": "localhost:6379"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
},
"AppSettings": {
"ApiBaseUrl": "https://api.example.com",
"PageSize": 25,
"Features": {
"EnableCaching": true,
"EnableMetrics": false
}
}
}
// Access nested values with colon separator
var defaultDb = configuration["ConnectionStrings:DefaultDb"];
var defaultLogLevel = configuration["Logging:LogLevel:Default"];
var enableCaching = configuration.GetValue<bool>("AppSettings:Features:EnableCaching");
The Options Pattern
Strongly-typed configuration that integrates with dependency injection.
Basic Options Setup
// Define options class
public class EmailOptions
{
public const string SectionName = "Email";
public string SmtpServer { get; set; } = "";
public int Port { get; set; } = 587;
public string FromAddress { get; set; } = "";
public bool UseSsl { get; set; } = true;
}
// appsettings.json
{
"Email": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"FromAddress": "noreply@example.com",
"UseSsl": true
}
}
// Register in Program.cs
builder.Services.Configure<EmailOptions>(
builder.Configuration.GetSection(EmailOptions.SectionName));
// Or bind manually
builder.Services.Configure<EmailOptions>(options =>
{
options.SmtpServer = "smtp.custom.com";
options.Port = 25;
});
Consuming Options
public class EmailService
{
private readonly EmailOptions _options;
// IOptions<T> - singleton, read at startup
public EmailService(IOptions<EmailOptions> options)
{
_options = options.Value;
}
public void SendEmail(string to, string subject, string body)
{
using var client = new SmtpClient(_options.SmtpServer, _options.Port);
client.EnableSsl = _options.UseSsl;
// ...
}
}
Options Interfaces
IOptions<T>
- Lifetime: Singleton
- Updates: No
- Use for: Static configuration
IOptionsSnapshot<T>
- Lifetime: Scoped
- Updates: Per request
- Use for: Config that changes between requests
IOptionsMonitor<T>
- Lifetime: Singleton
- Updates: Yes, with callback
- Use for: Live updates, long-running services
| Interface | Lifetime | Updates | Use Case |
|---|---|---|---|
IOptions<T> |
Singleton | No | Static configuration |
IOptionsSnapshot<T> |
Scoped | Per request | Config that changes between requests |
IOptionsMonitor<T> |
Singleton | Yes, with callback | Live updates, long-running services |
// IOptionsSnapshot - picks up changes per request
public class FeatureService
{
private readonly FeatureOptions _options;
public FeatureService(IOptionsSnapshot<FeatureOptions> options)
{
_options = options.Value; // Fresh value each request
}
}
// IOptionsMonitor - live updates with change notification
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerOptions> _optionsMonitor;
public BackgroundWorker(IOptionsMonitor<WorkerOptions> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
// Subscribe to changes
_optionsMonitor.OnChange(options =>
{
Console.WriteLine($"Options changed: Interval = {options.Interval}");
});
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var options = _optionsMonitor.CurrentValue; // Always current
await DoWorkAsync(options);
await Task.Delay(options.Interval, ct);
}
}
}
Named Options
For multiple configurations of the same type.
// appsettings.json
{
"HttpClients": {
"GitHub": {
"BaseUrl": "https://api.github.com",
"Timeout": 30
},
"Stripe": {
"BaseUrl": "https://api.stripe.com",
"Timeout": 60
}
}
}
// Register named options
builder.Services.Configure<HttpClientOptions>("GitHub",
builder.Configuration.GetSection("HttpClients:GitHub"));
builder.Services.Configure<HttpClientOptions>("Stripe",
builder.Configuration.GetSection("HttpClients:Stripe"));
// Consume with IOptionsSnapshot or IOptionsMonitor
public class ApiClientFactory
{
private readonly IOptionsSnapshot<HttpClientOptions> _options;
public ApiClientFactory(IOptionsSnapshot<HttpClientOptions> options)
{
_options = options;
}
public HttpClient CreateClient(string name)
{
var options = _options.Get(name); // Get by name
return new HttpClient
{
BaseAddress = new Uri(options.BaseUrl),
Timeout = TimeSpan.FromSeconds(options.Timeout)
};
}
}
Options Validation
Data Annotations
using System.ComponentModel.DataAnnotations;
public class DatabaseOptions
{
[Required]
public string ConnectionString { get; set; } = "";
[Range(1, 100)]
public int MaxConnections { get; set; } = 10;
[RegularExpression(@"^\d+$")]
public string PoolSize { get; set; } = "5";
}
// Enable validation
builder.Services.AddOptions<DatabaseOptions>()
.Bind(builder.Configuration.GetSection("Database"))
.ValidateDataAnnotations()
.ValidateOnStart(); // Fail fast at startup
Custom Validation
builder.Services.AddOptions<ApiOptions>()
.Bind(builder.Configuration.GetSection("Api"))
.Validate(options =>
{
if (string.IsNullOrEmpty(options.ApiKey))
return false;
if (options.Timeout <= TimeSpan.Zero)
return false;
return true;
}, "API configuration is invalid")
.ValidateOnStart();
// Complex validation with IValidateOptions
public class ApiOptionsValidator : IValidateOptions<ApiOptions>
{
public ValidateOptionsResult Validate(string? name, ApiOptions options)
{
var failures = new List<string>();
if (string.IsNullOrEmpty(options.BaseUrl))
failures.Add("BaseUrl is required");
if (!Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out _))
failures.Add("BaseUrl must be a valid URL");
if (options.RetryCount < 0 || options.RetryCount > 10)
failures.Add("RetryCount must be between 0 and 10");
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
// Register validator
builder.Services.AddSingleton<IValidateOptions<ApiOptions>, ApiOptionsValidator>();
Configuration Providers
JSON Files
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true, reloadOnChange: true);
Environment Variables
Environment Variable Naming Convention
Use double underscores (__) to represent nesting in hierarchical configuration. Example: MYAPP_ConnectionStrings__DefaultDb maps to configuration["ConnectionStrings:DefaultDb"]
// Automatic in Host builder
builder.Configuration.AddEnvironmentVariables();
// With prefix filter
builder.Configuration.AddEnvironmentVariables("MYAPP_");
// Environment variable naming (double underscore for nesting)
// MYAPP_ConnectionStrings__DefaultDb = "Server=..."
// Maps to configuration["ConnectionStrings:DefaultDb"]
User Secrets (Development)
# Initialize secrets for project
dotnet user-secrets init
# Set a secret
dotnet user-secrets set "Api:SecretKey" "my-secret-key"
# List secrets
dotnet user-secrets list
// Added automatically in Development
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
Command Line
builder.Configuration.AddCommandLine(args);
// Usage
// dotnet run --ConnectionStrings:DefaultDb="Server=prod"
// dotnet run /Settings:MaxRetries=5
In-Memory (Testing)
var config = new Dictionary<string, string?>
{
["Database:ConnectionString"] = "Server=test",
["Features:EnableNewUI"] = "true"
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(config)
.Build();
Custom Provider
public class DatabaseConfigurationProvider : ConfigurationProvider
{
private readonly string _connectionString;
public DatabaseConfigurationProvider(string connectionString)
{
_connectionString = connectionString;
}
public override void Load()
{
using var connection = new SqlConnection(_connectionString);
connection.Open();
using var command = new SqlCommand(
"SELECT [Key], [Value] FROM Configuration", connection);
using var reader = command.ExecuteReader();
var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
data[reader.GetString(0)] = reader.GetString(1);
}
Data = data;
}
}
public class DatabaseConfigurationSource : IConfigurationSource
{
public string ConnectionString { get; set; } = "";
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DatabaseConfigurationProvider(ConnectionString);
}
}
// Extension method
public static class ConfigurationExtensions
{
public static IConfigurationBuilder AddDatabase(
this IConfigurationBuilder builder,
string connectionString)
{
return builder.Add(new DatabaseConfigurationSource
{
ConnectionString = connectionString
});
}
}
// Usage
builder.Configuration.AddDatabase(connectionString);
Environment-Specific Configuration
Detecting Environment
// In Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
}
if (builder.Environment.IsProduction())
{
builder.Services.AddApplicationInsightsTelemetry();
}
// Custom environments
if (builder.Environment.IsEnvironment("Staging"))
{
// Staging-specific configuration
}
Environment Override Pattern
// appsettings.json (defaults)
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"Features": {
"EnableDebugEndpoints": false
}
}
// appsettings.Development.json (overrides)
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"Features": {
"EnableDebugEndpoints": true
}
}
// appsettings.Production.json (overrides)
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
Secrets Management
Development Secrets
// User secrets - never committed to source control
// Stored in: %APPDATA%\Microsoft\UserSecrets\{guid}\secrets.json (Windows)
// Stored in: ~/.microsoft/usersecrets/{guid}/secrets.json (Linux/macOS)
dotnet user-secrets set "Database:Password" "dev-password"
Production Secrets
// Azure Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri("https://myapp-vault.vault.azure.net/"),
new DefaultAzureCredential());
// AWS Secrets Manager (via custom provider or SDK)
// HashiCorp Vault (via custom provider)
Secret Reference Pattern
// appsettings.json - reference, not value
{
"Database": {
"ConnectionString": "from-keyvault"
}
}
// Key Vault secret named "Database--ConnectionString"
// Automatically mapped to configuration["Database:ConnectionString"]
Post-Configuration
Modify options after binding.
builder.Services.PostConfigure<EmailOptions>(options =>
{
// Always apply these after any other configuration
if (string.IsNullOrEmpty(options.FromAddress))
{
options.FromAddress = "default@example.com";
}
});
// Named post-configuration
builder.Services.PostConfigure<HttpClientOptions>("GitHub", options =>
{
options.BaseUrl = options.BaseUrl.TrimEnd('/');
});
// PostConfigureAll - applies to all named options
builder.Services.PostConfigureAll<HttpClientOptions>(options =>
{
if (options.Timeout == default)
{
options.Timeout = 30;
}
});
Configuration Binding
Bind to Existing Object
var settings = new AppSettings();
configuration.GetSection("AppSettings").Bind(settings);
// Or create and bind
var settings = configuration.GetSection("AppSettings").Get<AppSettings>();
Binding Options
builder.Services.Configure<ComplexOptions>(
builder.Configuration.GetSection("Complex"),
options =>
{
options.BindNonPublicProperties = true;
options.ErrorOnUnknownConfiguration = true; // Catch typos
});
Version History
| Feature | Version | Significance |
|---|---|---|
| IConfiguration | .NET Core 1.0 | Unified configuration API |
| Options pattern | .NET Core 1.0 | Strongly-typed configuration |
| IOptionsSnapshot | .NET Core 1.1 | Scoped options with reload |
| IOptionsMonitor | .NET Core 2.0 | Live options with change tokens |
| ValidateOnStart | .NET 6 | Fail-fast validation |
| Options validation | .NET Core 2.1 | Data annotations support |
Key Takeaways
Use the options pattern: Strongly-typed options integrate with DI and enable validation.
Layer configuration sources: Base settings in appsettings.json, environment-specific overrides, secrets from secure sources.
Never store secrets in code or appsettings.json: Use User Secrets for development, Key Vault or similar for production.
Validate early: Use ValidateOnStart() to catch configuration errors at startup, not at runtime.
Choose the right interface: IOptions<T> for static config, IOptionsSnapshot<T> for per-request, IOptionsMonitor<T> for live updates.
Environment variables for containers: Override configuration in Docker/Kubernetes via environment variables without rebuilding images.
Found this guide helpful? Share it with your team:
Share on LinkedIn