Skip to content

codingdroplets/dotnet-options-pattern-configuration

Repository files navigation

dotnet-options-pattern-configuration

Strongly-typed configuration in ASP.NET Core using IOptions, IOptionsSnapshot, IOptionsMonitor, and Named Options — with validation, hot-reload, and change notifications.

Visit CodingDroplets YouTube Patreon Buy Me a Coffee GitHub


🚀 Support the Channel — Join on Patreon

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 ☕


🎯 What You'll Learn

  • How the Options Pattern solves raw IConfiguration access 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 with ValidateOnStart()
  • 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

🗺️ Architecture Overview

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}

📋 Options Interface Comparison

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

📁 Project Structure

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)

🛠️ Prerequisites

Requirement Version
.NET SDK 10.0+
IDE Visual Studio 2022 v17.12+ / JetBrains Rider / VS Code
OS Windows / macOS / Linux

⚡ Quick Start

# 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/swagger

Visual Studio users: press F5 — the browser will open automatically at /swagger.


🔧 How It Works

1. Define a strongly-typed options class

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;
}

2. Bind and validate in Program.cs

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 use

3. Use IOptions<T> in a Singleton service (frozen at startup)

public sealed class EmailService : IEmailService
{
    private readonly SmtpOptions _smtp;

    public EmailService(IOptions<SmtpOptions> options)
    {
        _smtp = options.Value; // Captured once — never changes
    }
}

4. Use Named Options + IOptionsSnapshot<T> in a Scoped service

// 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);
    }
}

5. Use IOptionsMonitor<T> for live reload in a Singleton

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();
}

📡 API Endpoints

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

🧪 Running Tests

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 ✅

🤔 Key Concepts

Why not just use IConfiguration directly?

// ❌ 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;

ValidateOnStart vs lazy validation

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.

Why IOptionsSnapshot cannot be injected into Singletons

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

🏷️ Technologies Used

  • .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.DataAnnotationsValidateDataAnnotations() + ValidateOnStart()

📚 References


📄 License

This project is licensed under the MIT License. See LICENSE for details.


🔗 Connect with CodingDroplets

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!

About

ASP.NET Core Options Pattern demo: IOptions, IOptionsSnapshot, IOptionsMonitor, Named Options, data annotation validation and hot-reload in .NET 10 Web API.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages