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 API
cd OptionsPatternDemo
dotnet run
# 4. Open Swagger UI
# http://localhost:5289/swaggerVisual Studio users: press F5 — the browser will open automatically at
/swagger.
public sealed class SmtpOptions
{
public const string SectionName = "Smtp";
[Required] public string Host { get; set; } = string.Empty;
[Range(1, 65535)] public int Port { get; set; } = 587;
[Required][EmailAddress] public string SenderEmail { get; set; } = string.Empty;
public bool EnableSsl { 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 usepublic sealed class EmailService : IEmailService
{
private readonly SmtpOptions _smtp;
public EmailService(IOptions<SmtpOptions> options)
{
_smtp = options.Value; // Captured once — never changes
}
}// Register two named instances
builder.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 service
public sealed class CacheInfoService : ICacheInfoService
{
private readonly IOptionsSnapshot<CacheOptions> _cacheOptions;
public CacheInfoService(IOptionsSnapshot<CacheOptions> cacheOptions)
{
_cacheOptions = cacheOptions;
}
public CachePolicyInfo GetCachePolicy(string name)
{
var opts = _cacheOptions.Get(name); // "L1" or "L2"
return new CachePolicyInfo(name, opts.Provider, opts.DefaultTtlSeconds, opts.Enabled, opts.MaxItems);
}
}public sealed class FeatureFlagService : IFeatureFlagService, IDisposable
{
private readonly IOptionsMonitor<FeatureFlagOptions> _monitor;
private readonly IDisposable? _changeListener;
public FeatureFlagService(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));
}
public FeatureFlagSnapshot GetFlags()
{
var flags = _monitor.CurrentValue; // Always latest config — no restart needed
return new FeatureFlagSnapshot(flags.EnableDarkMode, flags.EnableBetaApi, flags.EnableAnalytics, flags.EnableNewDashboard);
}
public void Dispose() => _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 validation
var host = _configuration["Smtp:Host"];
var port = int.Parse(_configuration["Smtp:Port"]!);
// ✅ Options Pattern — strongly typed, validated, testable
var host = _smtp.Host;
var port = _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!