Microsoft.Extensions Configuration Patterns When to Use This Skill Use this skill when: Binding configuration from appsettings.json to strongly-typed classes Validating configuration at application startup (fail fast) Implementing complex validation logic for settings Designing configuration classes that are testable and maintainable Understanding IOptions, IOptionsSnapshot, and IOptionsMonitor Why Configuration Validation Matters The Problem: Applications often fail at runtime due to misconfiguration - missing connection strings, invalid URLs, out-of-range values. These failures happen deep in business logic, far from where configuration is loaded, making debugging difficult. The Solution: Validate configuration at startup. If configuration is invalid, the application fails immediately with a clear error message. This is the "fail fast" principle. // BAD: Fails at runtime when someone tries to use the service public class EmailService { public EmailService ( IOptions < SmtpSettings
options ) { var settings = options . Value ; // Throws NullReferenceException 10 minutes into production _client = new SmtpClient ( settings . Host , settings . Port ) ; } } // GOOD: Fails at startup with clear error // "SmtpSettings validation failed: Host is required" Pattern 1: Basic Options Binding Define a Settings Class public class SmtpSettings { public const string SectionName = "Smtp" ; public string Host { get ; set ; } = string . Empty ; public int Port { get ; set ; } = 587 ; public string ? Username { get ; set ; } public string ? Password { get ; set ; } public bool UseSsl { get ; set ; } = true ; } Bind from Configuration // In Program.cs or service registration builder . Services . AddOptions < SmtpSettings
( ) . BindConfiguration ( SmtpSettings . SectionName ) ; // appsettings.json { "Smtp" : { "Host" : "smtp.example.com" , "Port" : 587 , "Username" : "user@example.com" , "Password" : "secret" , "UseSsl" : true } } Consume in Services public class EmailService { private readonly SmtpSettings _settings ; // IOptions
- singleton, read once at startup public EmailService ( IOptions < SmtpSettings options ) { _settings = options . Value ; } } Pattern 2: Data Annotations Validation For simple validation rules, use Data Annotations: using System . ComponentModel . DataAnnotations ; public class SmtpSettings { public const string SectionName = "Smtp" ; [ Required ( ErrorMessage = "SMTP host is required" ) ] public string Host { get ; set ; } = string . Empty ; [ Range ( 1 , 65535 , ErrorMessage = "Port must be between 1 and 65535" ) ] public int Port { get ; set ; } = 587 ; [ EmailAddress ( ErrorMessage = "Username must be a valid email address" ) ] public string ? Username { get ; set ; } public string ? Password { get ; set ; } public bool UseSsl { get ; set ; } = true ; } Enable Data Annotations Validation builder . Services . AddOptions < SmtpSettings
( ) . BindConfiguration ( SmtpSettings . SectionName ) . ValidateDataAnnotations ( ) // Enable attribute-based validation . ValidateOnStart ( ) ; // Validate immediately at startup Key Point: .ValidateOnStart() is critical. Without it, validation only runs when the options are first accessed, which could be minutes or hours into application runtime. Pattern 3: IValidateOptions for Complex Validation Data Annotations work for simple rules, but complex validation requires IValidateOptions
: When to Use IValidateOptions Scenario Data Annotations IValidateOptions Required field ✅ ✅ Range check ✅ ✅ Regex pattern ✅ ✅ Cross-property validation ❌ ✅ Conditional validation ❌ ✅ External service checks ❌ ✅ Custom error messages with context Limited ✅ Dependency injection in validator ❌ ✅ Implementing IValidateOptions using Microsoft . Extensions . Options ; public class SmtpSettingsValidator : IValidateOptions < SmtpSettings { public ValidateOptionsResult Validate ( string ? name , SmtpSettings options ) { var failures = new List < string
( ) ; // Required field validation if ( string . IsNullOrWhiteSpace ( options . Host ) ) { failures . Add ( "Host is required" ) ; } // Range validation if ( options . Port is < 1 or
65535 ) { failures . Add ( $"Port { options . Port } is invalid. Must be between 1 and 65535" ) ; } // Cross-property validation if ( ! string . IsNullOrEmpty ( options . Username ) && string . IsNullOrEmpty ( options . Password ) ) { failures . Add ( "Password is required when Username is specified" ) ; } // Conditional validation if ( options . UseSsl && options . Port == 25 ) { failures . Add ( "Port 25 is typically not used with SSL. Consider port 465 or 587" ) ; } // Return result return failures . Count
0 ? ValidateOptionsResult . Fail ( failures ) : ValidateOptionsResult . Success ; } } Register the Validator builder . Services . AddOptions < SmtpSettings
( ) . BindConfiguration ( SmtpSettings . SectionName ) . ValidateDataAnnotations ( ) // Run attribute validation first . ValidateOnStart ( ) ; // Register the custom validator builder . Services . AddSingleton < IValidateOptions < SmtpSettings
, SmtpSettingsValidator
( ) ; Order matters: Data Annotations run first, then IValidateOptions validators. All failures are collected and reported together. Pattern 4: Validators with Dependencies IValidateOptions validators are resolved from DI, so they can have dependencies: public class DatabaseSettingsValidator : IValidateOptions < DatabaseSettings
{ private readonly ILogger < DatabaseSettingsValidator
_logger ; private readonly IHostEnvironment _environment ; public DatabaseSettingsValidator ( ILogger < DatabaseSettingsValidator
logger , IHostEnvironment environment ) { _logger = logger ; _environment = environment ; } public ValidateOptionsResult Validate ( string ? name , DatabaseSettings options ) { var failures = new List < string
( ) ; if ( string . IsNullOrWhiteSpace ( options . ConnectionString ) ) { failures . Add ( "ConnectionString is required" ) ; } // Environment-specific validation if ( _environment . IsProduction ( ) ) { if ( options . ConnectionString ?. Contains ( "localhost" ) == true ) { failures . Add ( "Production cannot use localhost database" ) ; } if ( ! options . ConnectionString ?. Contains ( "Encrypt=True" ) == true ) { _logger . LogWarning ( "Production database connection should use encryption" ) ; } } // Validate connection string format if ( ! string . IsNullOrEmpty ( options . ConnectionString ) ) { try { var builder = new SqlConnectionStringBuilder ( options . ConnectionString ) ; if ( string . IsNullOrEmpty ( builder . DataSource ) ) { failures . Add ( "ConnectionString must specify a Data Source" ) ; } } catch ( Exception ex ) { failures . Add ( $"ConnectionString is malformed: { ex . Message } " ) ; } } return failures . Count
0 ? ValidateOptionsResult . Fail ( failures ) : ValidateOptionsResult . Success ; } } Pattern 5: Named Options When you have multiple instances of the same settings type (e.g., multiple database connections): // appsettings.json { "Databases" : { "Primary" : { "ConnectionString" : "Server=primary;..." } , "Replica" : { "ConnectionString" : "Server=replica;..." } } } // Registration builder . Services . AddOptions < DatabaseSettings
( "Primary" ) . BindConfiguration ( "Databases:Primary" ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; builder . Services . AddOptions < DatabaseSettings
( "Replica" ) . BindConfiguration ( "Databases:Replica" ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; // Consumption public class DataService { private readonly DatabaseSettings _primary ; private readonly DatabaseSettings _replica ; public DataService ( IOptionsSnapshot < DatabaseSettings
options ) { _primary = options . Get ( "Primary" ) ; _replica = options . Get ( "Replica" ) ; } } Named Options Validator public class DatabaseSettingsValidator : IValidateOptions < DatabaseSettings
{ public ValidateOptionsResult Validate ( string ? name , DatabaseSettings options ) { var failures = new List < string
( ) ; var prefix = string . IsNullOrEmpty ( name ) ? "" : $"[ { name } ] " ; if ( string . IsNullOrWhiteSpace ( options . ConnectionString ) ) { failures . Add ( $" { prefix } ConnectionString is required" ) ; } // Name-specific validation if ( name == "Primary" && options . ReadOnly ) { failures . Add ( "Primary database cannot be read-only" ) ; } return failures . Count
0 ? ValidateOptionsResult . Fail ( failures ) : ValidateOptionsResult . Success ; } } Pattern 6: Options Lifetime Understanding the three options interfaces: Interface Lifetime Reloads on Change Use Case IOptions
Singleton No Static config, read once IOptionsSnapshot Scoped Yes (per request) Web apps needing fresh config IOptionsMonitor Singleton Yes (with callback) Background services, real-time updates IOptionsMonitor for Background Services public class BackgroundWorker : BackgroundService { private readonly IOptionsMonitor < WorkerSettings _optionsMonitor ; private WorkerSettings _currentSettings ; public BackgroundWorker ( IOptionsMonitor < WorkerSettings
optionsMonitor ) { _optionsMonitor = optionsMonitor ; _currentSettings = optionsMonitor . CurrentValue ; // Subscribe to configuration changes _optionsMonitor . OnChange ( settings => { _currentSettings = settings ; _logger . LogInformation ( "Worker settings updated: Interval={Interval}" , settings . PollingInterval ) ; } ) ; } protected override async Task ExecuteAsync ( CancellationToken stoppingToken ) { while ( ! stoppingToken . IsCancellationRequested ) { await DoWorkAsync ( ) ; await Task . Delay ( _currentSettings . PollingInterval , stoppingToken ) ; } } } Pattern 7: Post-Configuration Modify options after binding but before validation: builder . Services . AddOptions < ApiSettings
( ) . BindConfiguration ( "Api" ) . PostConfigure ( options => { // Ensure BaseUrl ends with / if ( ! string . IsNullOrEmpty ( options . BaseUrl ) && ! options . BaseUrl . EndsWith ( '/' ) ) { options . BaseUrl += '/' ; } // Set defaults based on environment options . Timeout ??= TimeSpan . FromSeconds ( 30 ) ; } ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; PostConfigure with Dependencies builder . Services . AddOptions < ApiSettings
( ) . BindConfiguration ( "Api" ) . PostConfigure < IHostEnvironment
( ( options , env ) => { if ( env . IsDevelopment ( ) ) { options . Timeout = TimeSpan . FromMinutes ( 5 ) ; // Longer timeout for debugging } } ) ; Pattern 8: Complete Example - Production Settings Class using System . ComponentModel . DataAnnotations ; using Microsoft . Extensions . Options ; public class AkkaSettings { public const string SectionName = "AkkaSettings" ; [ Required ] public string ActorSystemName { get ; set ; } = "MySystem" ; public AkkaExecutionMode ExecutionMode { get ; set ; } = AkkaExecutionMode . LocalTest ; public bool LogConfigOnStart { get ; set ; } = false ; public RemoteOptions RemoteOptions { get ; set ; } = new ( ) ; public ClusterOptions ClusterOptions { get ; set ; } = new ( ) ; public ClusterBootstrapOptions ClusterBootstrapOptions { get ; set ; } = new ( ) ; } public enum AkkaExecutionMode { LocalTest , // No remoting, no clustering Clustered // Full cluster with sharding, distributed pub/sub } public class AkkaSettingsValidator : IValidateOptions < AkkaSettings
{ private readonly IHostEnvironment _environment ; public AkkaSettingsValidator ( IHostEnvironment environment ) { _environment = environment ; } public ValidateOptionsResult Validate ( string ? name , AkkaSettings options ) { var failures = new List < string
( ) ; // Basic validation if ( string . IsNullOrWhiteSpace ( options . ActorSystemName ) ) { failures . Add ( "ActorSystemName is required" ) ; } // Mode-specific validation if ( options . ExecutionMode == AkkaExecutionMode . Clustered ) { ValidateClusteredMode ( options , failures ) ; } // Environment-specific validation if ( _environment . IsProduction ( ) && options . ExecutionMode == AkkaExecutionMode . LocalTest ) { failures . Add ( "LocalTest execution mode is not allowed in production" ) ; } return failures . Count
0 ? ValidateOptionsResult . Fail ( failures ) : ValidateOptionsResult . Success ; } private void ValidateClusteredMode ( AkkaSettings options , List < string
failures ) { if ( string . IsNullOrEmpty ( options . RemoteOptions . PublicHostName ) ) { failures . Add ( "RemoteOptions.PublicHostName is required in Clustered mode" ) ; } if ( options . RemoteOptions . Port is null or < 0 ) { failures . Add ( "RemoteOptions.Port must be >= 0 in Clustered mode" ) ; } if ( options . ClusterBootstrapOptions . Enabled ) { ValidateClusterBootstrap ( options . ClusterBootstrapOptions , failures ) ; } else if ( options . ClusterOptions . SeedNodes ?. Length == 0 ) { failures . Add ( "Either ClusterBootstrap must be enabled or SeedNodes must be specified" ) ; } } private void ValidateClusterBootstrap ( ClusterBootstrapOptions options , List < string
failures ) { if ( string . IsNullOrEmpty ( options . ServiceName ) ) { failures . Add ( "ClusterBootstrapOptions.ServiceName is required" ) ; } if ( options . RequiredContactPointsNr <= 0 ) { failures . Add ( "ClusterBootstrapOptions.RequiredContactPointsNr must be > 0" ) ; } switch ( options . DiscoveryMethod ) { case DiscoveryMethod . Config : if ( options . ConfigServiceEndpoints ?. Length == 0 ) { failures . Add ( "ConfigServiceEndpoints required for Config discovery" ) ; } break ; case DiscoveryMethod . AzureTableStorage : if ( options . AzureDiscoveryOptions == null ) { failures . Add ( "AzureDiscoveryOptions required for Azure discovery" ) ; } break ; } } } // Registration builder . Services . AddOptions < AkkaSettings
( ) . BindConfiguration ( AkkaSettings . SectionName ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; builder . Services . AddSingleton < IValidateOptions < AkkaSettings
, AkkaSettingsValidator
( ) ; Anti-Patterns to Avoid 1. Manual Configuration Access // BAD: Bypasses validation, hard to test public class MyService { public MyService ( IConfiguration configuration ) { var host = configuration [ "Smtp:Host" ] ; // No validation! } } // GOOD: Strongly-typed, validated public class MyService { public MyService ( IOptions < SmtpSettings
options ) { var host = options . Value . Host ; // Validated at startup } } 2. Validation in Constructor // BAD: Validation happens at runtime, not startup public class MyService { public MyService ( IOptions < Settings
options ) { if ( string . IsNullOrEmpty ( options . Value . Required ) ) throw new ArgumentException ( "Required is missing" ) ; // Too late! } } // GOOD: Validation at startup builder . Services . AddOptions < Settings
( ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; 3. Forgetting ValidateOnStart // BAD: Validation only runs when first accessed builder . Services . AddOptions < Settings
( ) . ValidateDataAnnotations ( ) ; // Missing ValidateOnStart! // GOOD: Fails immediately if invalid builder . Services . AddOptions < Settings
( ) . ValidateDataAnnotations ( ) . ValidateOnStart ( ) ; 4. Throwing in IValidateOptions // BAD: Throws exception, breaks validation chain public ValidateOptionsResult Validate ( string ? name , Settings options ) { if ( options . Value < 0 ) throw new ArgumentException ( "Value cannot be negative" ) ; // Wrong! return ValidateOptionsResult . Success ; } // GOOD: Return failure result public ValidateOptionsResult Validate ( string ? name , Settings options ) { if ( options . Value < 0 ) return ValidateOptionsResult . Fail ( "Value cannot be negative" ) ; return ValidateOptionsResult . Success ; } Testing Configuration Validators public class SmtpSettingsValidatorTests { private readonly SmtpSettingsValidator _validator = new ( ) ; [ Fact ] public void Validate_WithValidSettings_ReturnsSuccess ( ) { var settings = new SmtpSettings { Host = "smtp.example.com" , Port = 587 , Username = "user@example.com" , Password = "secret" } ; var result = _validator . Validate ( null , settings ) ; result . Succeeded . Should ( ) . BeTrue ( ) ; } [ Fact ] public void Validate_WithMissingHost_ReturnsFail ( ) { var settings = new SmtpSettings { Host = "" } ; var result = _validator . Validate ( null , settings ) ; result . Succeeded . Should ( ) . BeFalse ( ) ; result . FailureMessage . Should ( ) . Contain ( "Host is required" ) ; } [ Fact ] public void Validate_WithUsernameButNoPassword_ReturnsFail ( ) { var settings = new SmtpSettings { Host = "smtp.example.com" , Username = "user@example.com" , Password = null // Missing! } ; var result = _validator . Validate ( null , settings ) ; result . Succeeded . Should ( ) . BeFalse ( ) ; result . FailureMessage . Should ( ) . Contain ( "Password is required" ) ; } } Summary Principle Implementation Fail fast .ValidateOnStart() Strongly-typed Bind to POCO classes Simple validation Data Annotations Complex validation IValidateOptions
Cross-property rules IValidateOptions Environment-aware Inject IHostEnvironment Testable Validators are plain classes