| name | configuration |
| description | Use when setting up IConfiguration, Options pattern, appsettings layering, or user secrets.
|
| metadata | {"category":"core","agent":"dotnet-architect","when-to-use":"When configuring IOptions pattern, appsettings layering, or ValidateOnStart"} |
Configuration & Options Pattern
Core Principles
- Use strongly-typed Options classes instead of reading IConfiguration directly
- Validate options at startup with
ValidateOnStart() for fail-fast behavior
- Layer configuration:
appsettings.json < appsettings.{Environment}.json < env vars < user secrets
- Never store secrets in
appsettings.json — use user secrets (dev) or Key Vault (prod)
- Use
IOptions<T> for singleton config, IOptionsSnapshot<T> for scoped reloading
Patterns
Options Class with Validation
public sealed class DatabaseOptions
{
public const string SectionName = "Database";
[Required]
public required string ConnectionString { get; init; }
[Range(1, 100)]
public int MaxRetryCount { get; init; } = 3;
[Range(1, 3600)]
public int CommandTimeoutSeconds { get; init; } = 30;
}
public sealed class JwtOptions
{
public const string SectionName = "Jwt";
[Required]
public required string Issuer { get; init; }
[Required]
public required string Audience { get; init; }
[Required, MinLength(32)]
public required string Key { get; init; }
[Range(1, 1440)]
public int ExpiryMinutes { get; init; } = 60;
}
Registration with ValidateOnStart
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<JwtOptions>()
.BindConfiguration(JwtOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
Complex Validation with IValidateOptions
public sealed class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
{
public ValidateOptionsResult Validate(string? name, DatabaseOptions options)
{
var failures = new List<string>();
if (options.ConnectionString.Contains("password=",
StringComparison.OrdinalIgnoreCase)
&& !options.ConnectionString.Contains("Encrypt=true",
StringComparison.OrdinalIgnoreCase))
{
failures.Add(
"Connections with passwords must use Encrypt=true.");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
builder.Services.AddSingleton<
IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();
IOptions vs IOptionsSnapshot vs IOptionsMonitor
public sealed class StartupService(IOptions<DatabaseOptions> options)
{
private readonly DatabaseOptions _db = options.Value;
}
public sealed class RequestService(IOptionsSnapshot<DatabaseOptions> options)
{
private readonly DatabaseOptions _db = options.Value;
}
public sealed class MonitorService(IOptionsMonitor<DatabaseOptions> options)
{
public MonitorService(IOptionsMonitor<DatabaseOptions> options)
{
options.OnChange(newOptions =>
{
});
}
}
appsettings Layering
{
"Database": {
"MaxRetryCount": 3,
"CommandTimeoutSeconds": 30
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
{
"Database": {
"ConnectionString": "Server=localhost;Database={Domain}Db;Trusted_Connection=true;TrustServerCertificate=true"
}
}
User Secrets (Development)
dotnet user-secrets init
dotnet user-secrets set "Database:ConnectionString" "Server=localhost;..."
dotnet user-secrets set "Jwt:Key" "your-development-secret-key-min-32-chars"
Environment Variables
Database__ConnectionString="Server=prod-server;..."
Jwt__Key="production-secret-key"
Named Options
builder.Services.AddOptions<StorageOptions>("azure")
.BindConfiguration("Storage:Azure")
.ValidateDataAnnotations();
builder.Services.AddOptions<StorageOptions>("aws")
.BindConfiguration("Storage:Aws")
.ValidateDataAnnotations();
public sealed class StorageFactory(IOptionsSnapshot<StorageOptions> options)
{
public IStorageClient Create(string provider)
{
var config = options.Get(provider);
return provider switch
{
"azure" => new AzureBlobClient(config),
"aws" => new S3Client(config),
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
}
Anti-Patterns
var connStr = configuration["Database:ConnectionString"];
{
"Jwt": { "Key": "super-secret-key-DO-NOT-COMMIT" }
}
services.Configure<DatabaseOptions>(
configuration.GetSection("Database"));
public class ReloadableService(IOptions<FeatureFlags> options) { }
Detect Existing Patterns
- Search for
IOptions<, IOptionsSnapshot<, IOptionsMonitor< in constructors
- Check for
BindConfiguration or Bind calls in Program.cs
- Look for
ValidateOnStart or ValidateDataAnnotations calls
- Check
appsettings.json for configuration sections
- Look for direct
IConfiguration reads (configuration["Key"])
- Check for
UserSecretsId in .csproj (user secrets enabled)
Adding to Existing Project
- Create Options classes for each configuration section
- Replace direct IConfiguration reads with
IOptions<T> injection
- Add data annotation validation and
ValidateOnStart()
- Move secrets to user secrets —
dotnet user-secrets init then set values
- Add environment-specific appsettings files if missing
- Add IValidateOptions for complex cross-property validation
Decision Guide
| Scenario | Interface | Notes |
|---|
| Singleton service needs config | IOptions<T> | Reads once |
| Scoped service needs live config | IOptionsSnapshot<T> | Reloads per scope |
| React to config changes | IOptionsMonitor<T> | Change callbacks |
| Multiple named configs | IOptionsSnapshot<T> with .Get(name) | Named options |
| Startup validation | ValidateOnStart() | Fail-fast |
References