C# Configuration & Options Pattern - Cheatsheet
2026/02/043 min read
bookmark this
C# Configuration & Options Pattern — Cheatsheet
A quick, scannable one-page reference for configuration sources, Options pattern registration, lifetime decisions, and common patterns in ASP.NET Core.
Configuration Sources (Last Wins)
1. appsettings.json
2. appsettings.{Environment}.json (e.g., appsettings.Development.json)
3. User Secrets (Development only)
4. Environment Variables
5. Command-line arguments
Quick Start — Register & Consume
Register in Program.cs:
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
Inject & use:
public class EmailSender
{
private readonly EmailSettings _settings;
public EmailSender(IOptions<EmailSettings> options) => _settings = options.Value;
public void Send(string to) { /* use _settings */ }
}
Options Interfaces — Choose Your Lifetime
| Interface | Lifetime | Reloads | Use For |
|---|---|---|---|
IOptions<T> |
Singleton | No | Static settings (secrets, connection strings) |
IOptionsSnapshot<T> |
Scoped | Per-request | Changing settings in scoped services (feature flags) |
IOptionsMonitor<T> |
Singleton | Live + callback | Singletons that react to config changes |
Named Options — Multiple Configs of Same Type
// Register
builder.Services.Configure<BlobStorageSettings>("Images",
builder.Configuration.GetSection("Storage:Images"));
builder.Services.Configure<BlobStorageSettings>("Documents",
builder.Configuration.GetSection("Storage:Documents"));
// Consume
public class StorageService
{
public StorageService(IOptionsSnapshot<BlobStorageSettings> options)
{
var imageContainer = options.Get("Images").ContainerName;
var docContainer = options.Get("Documents").ContainerName;
}
}
Validation — Always Use .ValidateOnStart()
builder.Services
.AddOptions<EmailSettings>()
.BindConfiguration("EmailSettings")
.ValidateDataAnnotations()
.ValidateOnStart(); // ← Catches errors at startup, not first request
With data annotations:
public class EmailSettings
{
[Required]
public string SmtpHost { get; init; } = string.Empty;
[Range(1, 65535)]
public int SmtpPort { get; init; } = 587;
[EmailAddress]
public string FromAddress { get; init; } = string.Empty;
}
Custom inline validation:
builder.Services
.AddOptions<EmailSettings>()
.BindConfiguration("EmailSettings")
.Validate(s => !string.IsNullOrEmpty(s.SmtpHost) && s.SmtpPort is >= 1 and <= 65535,
"Invalid SMTP settings")
.ValidateOnStart();
Environment Variables
Use double underscore (__) as section separator:
export EmailSettings__SmtpHost=smtp.example.com
export EmailSettings__SmtpPort=587
export ConnectionStrings__Default="Server=prod;Database=MyApp"
PostConfigure — Computed Defaults
builder.Services.PostConfigure<EmailSettings>(settings =>
{
if (string.IsNullOrEmpty(settings.FromAddress))
settings.FromAddress = $"noreply@{settings.SmtpHost}";
});
User Secrets (Development Only)
# Initialize
dotnet user-secrets init
# Set a secret
dotnet user-secrets set "EmailSettings:ApiKey" "my-secret-key"
# Access via IConfiguration (no code change needed)
var apiKey = configuration["EmailSettings:ApiKey"];
Binding Patterns
Method 1 — Simple (no validation):
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
Method 2 — With validation (recommended):
builder.Services
.AddOptions<EmailSettings>()
.BindConfiguration("EmailSettings")
.ValidateDataAnnotations()
.ValidateOnStart();
Method 3 — Immediate binding (no DI):
var settings = builder.Configuration.GetSection("EmailSettings").Get<EmailSettings>()!;
IOptionsMonitor with Change Callback
public class CacheService : IDisposable
{
private CacheSettings _settings;
private readonly IDisposable? _changeListener;
public CacheService(IOptionsMonitor<CacheSettings> monitor)
{
_settings = monitor.CurrentValue;
_changeListener = monitor.OnChange(newSettings =>
{
_settings = newSettings;
Console.WriteLine($"Cache updated: TTL={_settings.TtlMinutes}m");
});
}
public void Dispose() => _changeListener?.Dispose();
}
Anti-Patterns & Fixes
| Problem | Fix |
|---|---|
IConfiguration everywhere (magic strings) |
Use IOptions<T> for type safety |
Secrets in appsettings.json |
Use User Secrets (dev) or env vars (prod) |
| No validation on options | Use ValidateDataAnnotations() + ValidateOnStart() |
IOptions<T> in Singleton needing live reload |
Use IOptionsMonitor<T> |
IOptionsSnapshot<T> in Singleton |
Runtime error — use IOptionsMonitor<T> |
Quick Checklist
- Config is in
appsettings.jsonwith environment overrides - Options class created with
public const string SectionName - Registered in Program.cs with
Configure<T>()orAddOptions<T>().BindConfiguration() - Validation configured with
ValidateDataAnnotations()andValidateOnStart() - Secrets stored in User Secrets (dev) or env vars (prod)
- Correct interface chosen:
IOptions<T>,IOptionsSnapshot<T>, orIOptionsMonitor<T> -
PostConfigure<T>()used for computed defaults if needed
See the full guide: C# Configuration & Options Pattern