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!