Options Pattern for .NET Configuration
Overview
The Options pattern provides strongly-typed access to configuration groups:
- IOptions<T> - Singleton, read once at startup
- IOptionsSnapshot<T> - Scoped, reloads per request
- IOptionsMonitor<T> - Singleton, real-time change notifications
Quick Reference
| Interface | Lifetime | Supports Reload | Named Options | Use Case |
|---|---|---|---|---|
IOptions<T> | Singleton | No | No | Static config |
IOptionsSnapshot<T> | Scoped | Yes | Yes | Request-scoped config |
IOptionsMonitor<T> | Singleton | Yes | Yes | Singleton services, change notifications |
Options Structure
/Application/Options/
├── DatabaseOptions.cs
├── JwtOptions.cs
├── CacheOptions.cs
├── EmailOptions.cs
└── FeatureFlagOptions.cs
Template: Basic Options Class
// src/{name}.application/Options/DatabaseOptions.cs
namespace {name}.application.options;
/// <summary>
/// Options classes should:
/// - Use the "Options" suffix
/// - Have public getters and setters
/// - Include validation via data annotations or IValidateOptions
/// </summary>
public sealed class DatabaseOptions
{
/// <summary>
/// Configuration section name in appsettings.json
/// </summary>
public const string SectionName = "Database";
/// <summary>
/// Connection string for the primary database
/// </summary>
public required string ConnectionString { get; set; }
/// <summary>
/// Maximum number of connections in the pool
/// </summary>
public int MaxPoolSize { get; set; } = 100;
/// <summary>
/// Minimum number of connections in the pool
/// </summary>
public int MinPoolSize { get; set; } = 5;
/// <summary>
/// Connection timeout in seconds
/// </summary>
public int ConnectionTimeout { get; set; } = 30;
/// <summary>
/// Enable connection pooling
/// </summary>
public bool EnablePooling { get; set; } = true;
/// <summary>
/// Enable query logging for debugging
/// </summary>
public bool EnableQueryLogging { get; set; } = false;
}
Corresponding appsettings.json
{
"Database": {
"ConnectionString": "Host=localhost;Database=mydb;Username=postgres;Password=secret",
"MaxPoolSize": 100,
"MinPoolSize": 5,
"ConnectionTimeout": 30,
"EnablePooling": true,
"EnableQueryLogging": false
}
}
Template: Options with Data Annotation Validation
// src/{name}.application/Options/JwtOptions.cs
using System.ComponentModel.DataAnnotations;
namespace {name}.application.options;
public sealed class JwtOptions
{
public const string SectionName = "Jwt";
[Required(ErrorMessage = "JWT Secret is required")]
[MinLength(32, ErrorMessage = "JWT Secret must be at least 32 characters")]
public required string Secret { get; set; }
[Required(ErrorMessage = "JWT Issuer is required")]
public required string Issuer { get; set; }
[Required(ErrorMessage = "JWT Audience is required")]
public required string Audience { get; set; }
[Range(1, 1440, ErrorMessage = "Access token expiration must be between 1 and 1440 minutes")]
public int AccessTokenExpirationMinutes { get; set; } = 15;
[Range(1, 43200, ErrorMessage = "Refresh token expiration must be between 1 and 43200 minutes")]
public int RefreshTokenExpirationMinutes { get; set; } = 10080; // 7 days
}
Template: Options with Custom Validation
// src/{name}.application/Options/CacheOptions.cs
namespace {name}.application.options;
public sealed class CacheOptions
{
public const string SectionName = "Cache";
public bool Enabled { get; set; } = true;
public string? RedisConnectionString { get; set; }
public int DefaultExpirationMinutes { get; set; } = 5;
public int SlidingExpirationMinutes { get; set; } = 2;
public string KeyPrefix { get; set; } = string.Empty;
}
// src/{name}.application/Options/Validation/CacheOptionsValidator.cs
using Microsoft.Extensions.Options;
namespace {name}.application.options.validation;
/// <summary>
/// Custom validation using IValidateOptions for complex rules
/// </summary>
public sealed class CacheOptionsValidator : IValidateOptions<CacheOptions>
{
public ValidateOptionsResult Validate(string? name, CacheOptions options)
{
var failures = new List<string>();
if (options.Enabled && string.IsNullOrWhiteSpace(options.RedisConnectionString))
{
failures.Add("RedisConnectionString is required when caching is enabled");
}
if (options.DefaultExpirationMinutes < 1)
{
failures.Add("DefaultExpirationMinutes must be at least 1");
}
if (options.SlidingExpirationMinutes >= options.DefaultExpirationMinutes)
{
failures.Add("SlidingExpirationMinutes must be less than DefaultExpirationMinutes");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
Template: Registration in DependencyInjection
// src/{name}.application/DependencyInjection.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using {name}.application.options;
using {name}.application.options.validation;
namespace {name}.application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(
this IServiceCollection services,
IConfiguration configuration)
{
// ═══════════════════════════════════════════════════════════════
// BASIC OPTIONS BINDING
// ═══════════════════════════════════════════════════════════════
services.Configure<DatabaseOptions>(
configuration.GetSection(DatabaseOptions.SectionName));
// ═══════════════════════════════════════════════════════════════
// OPTIONS WITH DATA ANNOTATION VALIDATION
// Validates at startup - fails fast if invalid
// ═══════════════════════════════════════════════════════════════
services.AddOptions<JwtOptions>()
.Bind(configuration.GetSection(JwtOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart(); // Validates immediately at startup
// ═══════════════════════════════════════════════════════════════
// OPTIONS WITH CUSTOM VALIDATION
// ═══════════════════════════════════════════════════════════════
services.AddOptions<CacheOptions>()
.Bind(configuration.GetSection(CacheOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IValidateOptions<CacheOptions>, CacheOptionsValidator>();
// ═══════════════════════════════════════════════════════════════
// OPTIONS WITH POST-CONFIGURE
// Modify options after binding
// ═══════════════════════════════════════════════════════════════
services.PostConfigure<DatabaseOptions>(options =>
{
// Apply environment-specific modifications
if (string.IsNullOrEmpty(options.ConnectionString))
{
options.ConnectionString = Environment.GetEnvironmentVariable("DATABASE_URL")
?? throw new InvalidOperationException("Database connection string not configured");
}
});
return services;
}
}
Template: Using IOptions<T> (Singleton Services)
// src/{name}.infrastructure/Services/JwtTokenService.cs
using Microsoft.Extensions.Options;
using {name}.application.options;
namespace {name}.infrastructure.services;
/// <summary>
/// Use IOptions<T> for:
/// - Singleton services
/// - Configuration that doesn't change at runtime
/// - Best performance (read once, cached forever)
///