Strongly-typed configuration in ASP.NET Core using IOptions, IOptionsSnapshot, IOptionsMonitor, and Named Options — with validation, hot-reload, and change notifications.
If this sample saved you time, consider joining our Patreon community. You'll get exclusive .NET tutorials, premium code samples, and early access to new content — all for the price of a coffee.
👉 Join CodingDroplets on Patreon
Prefer a one-time tip? Buy us a coffee ☕
- How the Options Pattern solves raw
IConfigurationaccess anti-patterns - The difference between IOptions, IOptionsSnapshot, and IOptionsMonitor — and when to use each
- How to use Named Options to configure multiple independent instances of the same class
- How to add data annotation validation (
[Required],[Range],[EmailAddress]) and fail-fast at startup withValidateOnStart() - How to enable hot-reload of configuration without restarting the application
- How to subscribe to runtime change notifications via
IOptionsMonitor.OnChange - How to unit test each options interface without a running host
appsettings.json / appsettings.Development.json
│
▼
IConfiguration (file watcher enabled)
│
├─── Smtp section ──────► IOptions<SmtpOptions> ── Singleton (frozen at startup)
│ │
│ ▼
│ EmailService (Singleton)
│ GET /api/smtp
│
├─── Cache:L1 section ──► IOptionsSnapshot<CacheOptions>("L1") ┐
│ ├─ Scoped, reloads per request
├─── Cache:L2 section ──► IOptionsSnapshot<CacheOptions>("L2") ┘
│ │
│ ▼
│ CacheInfoService (Scoped)
│ GET /api/cache | /api/cache/l1 | /api/cache/l2
│
└─── FeatureFlags section ► IOptionsMonitor<FeatureFlagOptions> ── Singleton + live change events
│
▼
FeatureFlagService (Singleton)
GET /api/featureflags | /api/featureflags/{flag}
| Interface | Lifetime | Picks up config reload? | When to use |
|---|---|---|---|
IOptions<T> | Singleton | ❌ Never | Settings that never change at runtime (DB connection strings, JWT secrets) |
IOptionsSnapshot<T> | Scoped | ✅ Next request | Per-request settings, named options, feature config that can reload between requests |
IOptionsMonitor<T> | Singleton | ✅ Immediately | Singleton services needing live config, change event callbacks, feature flags |
IOptionsFactory<T> | Transient | ✅ Always | Manual resolution; rarely used directly |
dotnet-options-pattern-configuration/
├── dotnet-options-pattern-configuration.sln
│
├── OptionsPatternDemo/ # ASP.NET Core Web API (.NET 10)
│ ├── Controllers/
│ │ ├── SmtpController.cs # GET /api/smtp → IOptions demo
│ │ ├── CacheController.cs # GET /api/cache → IOptionsSnapshot + Named Options
│ │ └── FeatureFlagsController.cs # GET /api/featureflags → IOptionsMonitor demo
│ ├── Options/
│ │ ├── SmtpOptions.cs # Strongly-typed SMTP config with [Required]/[Range]
│ │ ├── CacheOptions.cs # Cache policy config (L1/L2 named instances)
│ │ └── FeatureFlagOptions.cs # Boolean feature flags (hot-reloadable)
│ ├── Services/
│ │ ├── EmailService.cs # Uses IOptions<SmtpOptions>
│ │ ├── CacheInfoService.cs # Uses IOptionsSnapshot<CacheOptions>
│ │ └── FeatureFlagService.cs # Uses IOptionsMonitor<FeatureFlagOptions>
│ ├── Properties/
│ │ └── launchSettings.json # Swagger opens on launch
│ ├── appsettings.json # Production config
│ ├── appsettings.Development.json # Dev overrides (hot-reload enabled)
│ └── Program.cs # DI registrations + Swagger setup
│
└── OptionsPatternDemo.Tests/ # xUnit test project
├── SmtpOptionsTests.cs # Validation + EmailService unit tests (9 tests)
├── CacheOptionsTests.cs # Validation + named options tests (5 tests)
└── FeatureFlagTests.cs # IOptionsMonitor unit tests (2 tests)
| Requirement | Version |
|---|---|
| .NET SDK | 10.0+ |
| IDE | Visual Studio 2022 v17.12+ / JetBrains Rider / VS Code |
| OS | Windows / macOS / Linux |
# 1. Clone the repository
git clone https://github.com/codingdroplets/dotnet-options-pattern-configuration.git
cd dotnet-options-pattern-configuration
# 2. Build the solution
dotnet build -c Release
# 3. Run the APIcd OptionsPatternDemo
dotnet run
# 4. Open Swagger UI# http://localhost:5289/swaggerVisual Studio users: press F5 — the browser will open automatically at
/swagger.
publicsealedclassSmtpOptions{publicconststringSectionName="Smtp";[Required]publicstringHost{get;set;}=string.Empty;[Range(1,65535)]publicintPort{get;set;}=587;[Required][EmailAddress]publicstringSenderEmail{get;set;}=string.Empty;publicboolEnableSsl{get;set;}=true;}builder.Services.AddOptions<SmtpOptions>().Bind(builder.Configuration.GetSection(SmtpOptions.SectionName)).ValidateDataAnnotations()// Validates [Required], [Range], [EmailAddress] etc..ValidateOnStart();// Fails fast at startup — not lazily on first usepublicsealedclassEmailService:IEmailService{privatereadonlySmtpOptions_smtp;publicEmailService(IOptions<SmtpOptions>options){_smtp=options.Value;// Captured once — never changes}}// Register two named instancesbuilder.Services.AddOptions<CacheOptions>("L1").Bind(builder.Configuration.GetSection("Cache:L1"));builder.Services.AddOptions<CacheOptions>("L2").Bind(builder.Configuration.GetSection("Cache:L2"));// Consume in a Scoped servicepublicsealedclassCacheInfoService:ICacheInfoService{privatereadonlyIOptionsSnapshot<CacheOptions>_cacheOptions;publicCacheInfoService(IOptionsSnapshot<CacheOptions>cacheOptions){_cacheOptions=cacheOptions;}publicCachePolicyInfoGetCachePolicy(stringname){varopts=_cacheOptions.Get(name);// "L1" or "L2"returnnewCachePolicyInfo(name,opts.Provider,opts.DefaultTtlSeconds,opts.Enabled,opts.MaxItems);}}publicsealedclassFeatureFlagService:IFeatureFlagService,IDisposable{privatereadonlyIOptionsMonitor<FeatureFlagOptions>_monitor;privatereadonlyIDisposable?_changeListener;publicFeatureFlagService(IOptionsMonitor<FeatureFlagOptions>monitor,ILogger<FeatureFlagService>logger){_monitor=monitor;// Fires whenever appsettings.json changes on disk_changeListener=_monitor.OnChange((opts,_)=>logger.LogInformation("Feature flags reloaded: BetaApi={BetaApi}",opts.EnableBetaApi));}publicFeatureFlagSnapshotGetFlags(){varflags=_monitor.CurrentValue;// Always latest config — no restart neededreturnnewFeatureFlagSnapshot(flags.EnableDarkMode,flags.EnableBetaApi,flags.EnableAnalytics,flags.EnableNewDashboard);}publicvoidDispose()=>_changeListener?.Dispose();}| Method | Endpoint | Description | Status |
|---|---|---|---|
GET | /api/smtp | Returns SMTP config via IOptions<T> (singleton snapshot) | 200 OK |
GET | /api/cache | Returns both L1 + L2 cache policies | 200 OK |
GET | /api/cache/l1 | Returns L1 (InMemory) cache policy via named options | 200 OK |
GET | /api/cache/l2 | Returns L2 (Redis) cache policy via named options | 200 OK |
GET | /api/featureflags | Returns all feature flags via IOptionsMonitor<T> | 200 OK |
GET | /api/featureflags/{flag} | Returns a single feature flag by name | 200 OK / 404 Not Found |
dotnet test -c Release| Test Class | Tests | What's Covered |
|---|---|---|
SmtpOptionsTests | 9 | Valid config passes, missing Host fails, invalid email fails, port boundary validation, EmailService unit test |
CacheOptionsTests | 5 | Valid config passes, missing Provider fails, TTL boundary validation, L1/L2 named options resolution |
FeatureFlagTests | 2 | All flags disabled, all flags enabled — IOptionsMonitor with fake monitor |
| Total | 16 | All passing ✅ |
// ❌ Anti-pattern — magic strings, no type safety, no validationvarhost=_configuration["Smtp:Host"];varport=int.Parse(_configuration["Smtp:Port"]!);// ✅ Options Pattern — strongly typed, validated, testablevarhost=_smtp.Host;varport=_smtp.Port;By default, options are validated the first time .Value is accessed. ValidateOnStart() moves that check to app startup so misconfiguration is caught immediately — before any request is served.
IOptionsSnapshot<T> is registered as Scoped. Injecting a Scoped dependency into a Singleton creates a captive dependency — the snapshot is frozen at the time the Singleton is first resolved and will never update.
| Service Lifetime | Compatible Interfaces |
|---|---|
| Singleton | IOptions<T>, IOptionsMonitor<T> |
| Scoped | IOptions<T>, IOptionsSnapshot<T>, IOptionsMonitor<T> |
| Transient | All |
- .NET 10 — target framework
- ASP.NET Core Web API — controller-based REST API
- Microsoft.Extensions.Options — IOptions / IOptionsSnapshot / IOptionsMonitor
- Swashbuckle.AspNetCore 6.x — Swagger / OpenAPI documentation
- Data Annotations —
[Required],[Range],[EmailAddress]for options validation - xUnit — unit testing framework
- Microsoft.Extensions.Options.DataAnnotations —
ValidateDataAnnotations()+ValidateOnStart()
- Options pattern in ASP.NET Core — Microsoft Learn
- Configuration in ASP.NET Core — Microsoft Learn
- Use IOptions, IOptionsSnapshot, and IOptionsMonitor — .NET Blog
This project is licensed under the MIT License. See LICENSE for details.
| Platform | Link |
|---|---|
| 🌐 Website | https://codingdroplets.com/ |
| 📺 YouTube | https://www.youtube.com/@CodingDroplets |
| 🎁 Patreon | https://www.patreon.com/CodingDroplets |
| ☕ Buy Me a Coffee | https://buymeacoffee.com/codingdroplets |
| 💻 GitHub | http://github.com/codingdroplets/ |
Want more samples like this?Support us on Patreon or buy us a coffee ☕ — every bit helps keep the content coming!