$npx -y skills add wshaddix/dotnet-skills --skill dotnet-csharp-configurationUsing Options pattern, user secrets, or feature flags. IOptions<T> and FeatureManagement.
| 1 | # dotnet-csharp-configuration |
| 2 | |
| 3 | Configuration patterns for .NET applications using Microsoft.Extensions.Configuration and Microsoft.Extensions.Options. Covers the Options pattern (`IOptions<T>`, `IOptionsMonitor<T>`, `IOptionsSnapshot<T>`), validation, user secrets, environment-based configuration, and feature flags with `Microsoft.FeatureManagement`. |
| 4 | |
| 5 | Cross-references: [skill:dotnet-csharp-dependency-injection] for service registration patterns, [skill:dotnet-csharp-coding-standards] for naming conventions. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Configuration Sources and Precedence |
| 10 | |
| 11 | Default configuration sources in `WebApplication.CreateBuilder` (last wins): |
| 12 | |
| 13 | 1. `appsettings.json` |
| 14 | 2. `appsettings.{Environment}.json` |
| 15 | 3. User secrets (Development only) |
| 16 | 4. Environment variables |
| 17 | 5. Command-line arguments |
| 18 | |
| 19 | ```csharp |
| 20 | var builder = WebApplication.CreateBuilder(args); |
| 21 | // Sources above are loaded automatically. Add custom sources: |
| 22 | builder.Configuration.AddJsonFile("features.json", optional: true, reloadOnChange: true); |
| 23 | ``` |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Options Pattern |
| 28 | |
| 29 | Bind configuration sections to strongly typed classes and inject them via DI. |
| 30 | |
| 31 | ### Defining Options Classes |
| 32 | |
| 33 | ```csharp |
| 34 | public sealed class SmtpOptions |
| 35 | { |
| 36 | public const string SectionName = "Smtp"; |
| 37 | |
| 38 | public string Host { get; set; } = ""; |
| 39 | public int Port { get; set; } = 587; |
| 40 | public string FromAddress { get; set; } = ""; |
| 41 | public bool UseSsl { get; set; } = true; |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | > Options classes use `{ get; set; }` (not `init`) because the configuration binder and `PostConfigure` need to mutate properties. Use `[Required]` via data annotations for mandatory fields instead. |
| 46 | |
| 47 | ### Registration |
| 48 | |
| 49 | ```csharp |
| 50 | builder.Services |
| 51 | .AddOptions<SmtpOptions>() |
| 52 | .BindConfiguration(SmtpOptions.SectionName) |
| 53 | .ValidateDataAnnotations() |
| 54 | .ValidateOnStart(); |
| 55 | ``` |
| 56 | |
| 57 | ### `appsettings.json` |
| 58 | |
| 59 | ```json |
| 60 | { |
| 61 | "Smtp": { |
| 62 | "Host": "smtp.example.com", |
| 63 | "Port": 587, |
| 64 | "FromAddress": "noreply@example.com", |
| 65 | "UseSsl": true |
| 66 | } |
| 67 | } |
| 68 | ``` |
| 69 | |
| 70 | --- |
| 71 | |
| 72 | ## Options Interfaces |
| 73 | |
| 74 | | Interface | Lifetime | Reload Behavior | Use Case | |
| 75 | |-----------|----------|-----------------|----------| |
| 76 | | `IOptions<T>` | Singleton | Never reloads after startup | Static config, most services | |
| 77 | | `IOptionsSnapshot<T>` | Scoped | Reloads per request/scope | Per-request config in ASP.NET | |
| 78 | | `IOptionsMonitor<T>` | Singleton | Live reload + change notification | Singletons, background services | |
| 79 | |
| 80 | ### Injection Examples |
| 81 | |
| 82 | ```csharp |
| 83 | // Static -- most common, singleton-safe |
| 84 | public sealed class EmailService(IOptions<SmtpOptions> options) |
| 85 | { |
| 86 | private readonly SmtpOptions _smtp = options.Value; |
| 87 | |
| 88 | public Task SendAsync(string to, string subject, string body, |
| 89 | CancellationToken ct = default) |
| 90 | { |
| 91 | // Use _smtp.Host, _smtp.Port, etc. |
| 92 | return Task.CompletedTask; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | // Live reload in singletons -- monitors config file changes |
| 97 | public sealed class FeatureService(IOptionsMonitor<FeatureOptions> monitor) |
| 98 | { |
| 99 | public bool IsEnabled(string feature) |
| 100 | => monitor.CurrentValue.EnabledFeatures.Contains(feature); |
| 101 | } |
| 102 | |
| 103 | // Per-request in scoped services -- reads latest config each request |
| 104 | public sealed class PricingService(IOptionsSnapshot<PricingOptions> snapshot) |
| 105 | { |
| 106 | public decimal GetMarkup() => snapshot.Value.MarkupPercent; |
| 107 | } |
| 108 | ``` |
| 109 | |
| 110 | ### Change Notifications with `IOptionsMonitor<T>` |
| 111 | |
| 112 | ```csharp |
| 113 | public sealed class CacheService : IDisposable |
| 114 | { |
| 115 | private readonly IDisposable? _changeListener; |
| 116 | private CacheOptions _current; |
| 117 | |
| 118 | public CacheService(IOptionsMonitor<CacheOptions> monitor) |
| 119 | { |
| 120 | _current = monitor.CurrentValue; |
| 121 | _changeListener = monitor.OnChange(updated => |
| 122 | { |
| 123 | _current = updated; |
| 124 | // React to config change -- flush cache, resize pool, etc. |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | public void Dispose() => _changeListener?.Dispose(); |
| 129 | } |
| 130 | ``` |
| 131 | |
| 132 | --- |
| 133 | |
| 134 | ## Options Validation |
| 135 | |
| 136 | ### Data Annotations |
| 137 | |
| 138 | ```csharp |
| 139 | using System.ComponentModel.DataAnnotations; |
| 140 | |
| 141 | public sealed class SmtpOptions |
| 142 | { |
| 143 | public const string SectionName = "Smtp"; |
| 144 | |
| 145 | [Required, MinLength(1)] |
| 146 | public string Host { get; set; } = ""; |
| 147 | |
| 148 | [Range(1, 65535)] |
| 149 | public int Port { get; set; } = 587; |
| 150 | |
| 151 | [Required, EmailAddress] |
| 152 | public string FromAddress { get; set; } = ""; |
| 153 | } |
| 154 | |
| 155 | builder.Services |
| 156 | .AddOptions<SmtpOptions>() |
| 157 | .BindConfiguration(SmtpOptions.SectionName) |
| 158 | .ValidateDataAnnotations() |
| 159 | .ValidateOnStart(); // Fail fast at startup, not on first use |
| 160 | ``` |
| 161 | |
| 162 | ### `IValidateOptions<T>` (Complex Validation) |
| 163 | |
| 164 | Use when validation logic requires cross-property checks or external dependencies. |
| 165 | |
| 166 | ```csharp |
| 167 | public sealed class SmtpOptionsValidator : IValidateOptions<SmtpOptions> |
| 168 | { |
| 169 | public ValidateOptionsResult Validate(string? name, SmtpOptions options) |
| 170 | { |
| 171 | var failures = new List<string>(); |
| 172 | |
| 173 | if (options.UseSsl && options.Port == 25) |
| 174 | { |
| 175 | failures.A |