seOptions
{
public const string SectionName = "Database";
[Required]
public string ConnectionString { get; init; }
[Range(1, 100)]
public int MaxPoolSize { get; init; } = 100;
[Required]
public TimeSpan CommandTimeout { get; init; } = TimeSpan.FromSeconds(30);
}
public class FeatureFlagsOptions
{
public const string SectionName = "FeatureFlags";
public bool EnableDarkMode { get; init; }
public bool EnableNewCheckout { get; init; }
}
### 2. Registration and Validation
Register options in `Program.cs` (or `Startup.cs`). Use `AddOptions` to chain binding and validation. Validation runs at startup, failing fast if configuration is invalid.
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register DatabaseOptions with validation
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// Register FeatureFlags with post-configuration logic
builder.Services.AddOptions<FeatureFlagsOptions>()
.BindConfiguration(FeatureFlagsOptions.SectionName)
.PostConfigure(options =>
{
// Apply business rules or defaults based on environment
if (builder.Environment.IsProduction())
{
options.EnableNewCheckout = false;
}
});
3. Injection Strategies
Select the injection interface based on usage requirements.
IOptions<T>: Singleton. Cached at startup. Use for static configuration that never changes.
IOptionsSnapshot<T>: Scoped. Re-evaluated per request. Use for configuration that changes during request processing or when using multiple configuration sources.
IOptionsMonitor<T>: Singleton. Supports hot-reload and change notifications. Use for background services and dynamic configuration.
Example: Consuming Dynamic Configuration in a Background Service
public class DynamicScalingWorker : BackgroundService
{
private readonly IOptionsMonitor<DatabaseOptions> _dbOptions;
private readonly ILogger<DynamicScalingWorker> _logger;
public DynamicScalingWorker(IOptionsMonitor<DatabaseOptions> dbOptions, ILogger<DynamicScalingWorker> logger)
{
_dbOptions = dbOptions;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Subscribe to changes
_dbOptions.OnChange(OnDatabaseConfigChanged);
while (!stoppingToken.IsCancellationRequested)
{
// Access current value; monitor handles reloading
var currentTimeout = _dbOptions.CurrentValue.CommandTimeout;
_logger.LogInformation("Running with timeout: {Timeout}", currentTimeout);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
private void OnDatabaseConfigChanged(DatabaseOptions options)
{
_logger.LogWarning("Database configuration updated. New timeout: {Timeout}", options.CommandTimeout);
}
}
For scenarios requiring configuration of multiple instances of the same type (e.g., multiple database connections), use IConfigureNamedOptions.
public class MultiDbOptionsSetup : IConfigureNamedOptions<DatabaseOptions>
{
private readonly IConfiguration _configuration;
public MultiDbOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(string? name, DatabaseOptions options)
{
if (name == "ReportingDb")
{
_configuration.GetSection("ReportingDatabase").Bind(options);
}
else if (name == "PrimaryDb")
{
_configuration.GetSection("PrimaryDatabase").Bind(options);
}
}
public void Configure(DatabaseOptions options) => Configure(Options.DefaultName, options);
}
// Registration
builder.Services.ConfigureOptions<MultiDbOptionsSetup>();
5. Secret Management
Never store secrets in appsettings.json. Use environment variables or a secret manager (e.g., Azure Key Vault, HashiCorp Vault) mapped to configuration keys.
// In Program.cs
builder.Configuration.AddAzureKeyVault(
new Uri(builder.Configuration["KeyVaultEndpoint"]),
new DefaultAzureCredential());
// Map Key Vault secrets to configuration keys
// Key Vault Secret: "Database--ConnectionString"
// Maps to config key: "Database:ConnectionString"
Pitfall Guide
1. Injecting IConfiguration into Services
Mistake: Services depend on IConfiguration and call GetSection("Key") internally.
Consequence: Breaks dependency inversion, hides configuration dependencies, prevents validation, and makes unit testing difficult.
Best Practice: Always inject IOptions<T>, IOptionsSnapshot<T>, or IOptionsMonitor<T>. Services should know only about strongly-typed options.
2. Ignoring Validation
Mistake: Relying on default values or null checks at runtime.
Consequence: Applications start successfully but fail unpredictably when accessing invalid configuration.
Best Practice: Use .ValidateDataAnnotations() or .Validate() and .ValidateOnStart(). Fail fast at startup to prevent deployment of broken configurations.
3. Using IOptions<T> for Dynamic Data
Mistake: Injecting IOptions<T> in a scenario where configuration changes at runtime.
Consequence: The service receives stale data because IOptions<T> is a singleton cache.
Best Practice: Use IOptionsMonitor<T> for singleton services requiring updates, or IOptionsSnapshot<T> for scoped services.
4. Hardcoding Section Names
Mistake: Using magic strings like "MySettings" throughout the codebase.
Consequence: Typos cause silent failures; refactoring section names in JSON breaks the app without compile-time errors.
Best Practice: Define constants in the options class (e.g., public const string SectionName = "MySettings";) and reference them during binding.
5. Over-Engineering Custom Providers
Mistake: Writing custom IConfigurationProvider implementations for data sources that can be handled by existing providers or IConfigureOptions.
Consequence: Increased complexity, maintenance burden, and potential bugs in configuration loading.
Best Practice: Use IConfigureOptions<T> for computed configuration. Reserve custom providers for non-standard sources like databases or custom APIs that require complex loading logic.
6. Missing Reload Tokens
Mistake: Using file-based configuration without enabling reload on change, or disabling it for performance without justification.
Consequence: Lost ability to hot-reload configuration, forcing restarts.
Best Practice: Ensure reloadOnChange: true is set for file providers in development/staging. In production, verify that the configuration source supports reloading (e.g., Azure App Configuration, Key Vault) before relying on IOptionsMonitor.
7. Binding to Mutable Objects
Mistake: Configuration POCOs have public setters, allowing services to modify configuration at runtime.
Consequence: Configuration state corruption; changes in one service affect others unexpectedly.
Best Practice: Use init only setters or private setters in options classes to enforce immutability after binding.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static Settings (e.g., App Name) | IOptions<T> | Lowest overhead; cached singleton. | Zero |
| Request-Specific Config | IOptionsSnapshot<T> | Scoped lifecycle; safe for multi-tenant. | Low |
| Dynamic Scaling / Feature Flags | IOptionsMonitor<T> | Hot-reload; change notifications. | Medium (Network calls for reload) |
| Secrets Management | Key Vault / Env Vars | Security compliance; audit trails. | Low (Cloud service cost) |
| Complex Computed Config | IConfigureNamedOptions | Decouples logic from binding. | Low |
| Multi-Instance Config | Named Options Pattern | Supports multiple configurations of same type. | Low |
Configuration Template
Copy this template into Program.cs to establish a robust configuration foundation.
using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
// 1. Add Configuration Sources
// Example: Azure Key Vault for secrets
if (builder.Configuration["UseKeyVault"] == "true")
{
builder.Configuration.AddAzureKeyVault(
new Uri(builder.Configuration["KeyVault:Endpoint"]),
new DefaultAzureCredential());
}
// 2. Register Options with Validation
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<AppSettingsOptions>()
.BindConfiguration(AppSettingsOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// 3. Register Services
builder.Services.AddHostedService<DynamicWorker>();
var app = builder.Build();
// 4. Verify Options at Startup (Optional but recommended)
using (var scope = app.Services.CreateScope())
{
var dbOptions = scope.ServiceProvider.GetRequiredService<IOptions<DatabaseOptions>>();
app.Logger.LogInformation("Database configured: {ConnStringMasked}",
dbOptions.Value.ConnectionString?.Substring(0, 10) + "...");
}
app.Run();
// Options Classes
public class DatabaseOptions
{
public const string SectionName = "Database";
[Required]
public string ConnectionString { get; init; } = string.Empty;
[Range(1, 500)]
public int MaxRetries { get; init; } = 3;
}
public class AppSettingsOptions
{
public const string SectionName = "AppSettings";
public bool EnableCaching { get; init; }
public string EnvironmentName { get; init; } = "Development";
}
Quick Start Guide
- Install Packages: Ensure
Microsoft.Extensions.Options and Microsoft.Extensions.Options.DataAnnotations are referenced.
- Create Options Class: Define a POCO with
init properties and validation attributes. Add a SectionName constant.
- Register in DI: In
Program.cs, call services.AddOptions<MyOptions>().BindConfiguration(MyOptions.SectionName).ValidateDataAnnotations().ValidateOnStart();.
- Inject and Use: Inject
IOptionsMonitor<MyOptions> (or appropriate interface) into your service and access .CurrentValue.
- Verify: Run the application. If
appsettings.json is missing required fields or values are out of range, the app will throw an exception at startup, preventing deployment of invalid configuration.