IOptions Pattern — Strongly-Typed Configuration Binding

The IOptions pattern is ASP.NET Core’s strongly-typed approach to configuration. Instead of reading raw strings from IConfiguration throughout your code (config["Smtp:Host"]), you define a POCO class with the configuration properties, bind it to a configuration section, and inject it as IOptions<SmtpOptions>. This gives you IntelliSense, compile-time type safety, validation at startup, and testability — you can construct the options object directly in tests without a full configuration pipeline. The IOptions pattern should be used for every configurable component in production ASP.NET Core applications.

Defining and Binding Options

// ── 1. Define a strongly-typed options class ──────────────────────────────
public class SmtpOptions
{
    public const string SectionName = "Smtp";   // conventional section name constant

    [Required]
    public string Host     { get; init; } = string.Empty;

    [Range(1, 65535)]
    public int    Port     { get; init; } = 587;

    [Required]
    public string Username { get; init; } = string.Empty;

    [Required]
    public string Password { get; init; } = string.Empty;

    public bool   UseSsl   { get; init; } = true;
}

// ── appsettings.json ───────────────────────────────────────────────────────
// {
//   "Smtp": {
//     "Host": "smtp.sendgrid.net",
//     "Port": 587,
//     "Username": "apikey",
//     "Password": "SG.xxx",
//     "UseSsl": true
//   }
// }

// ── 2. Register in Program.cs ─────────────────────────────────────────────
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration(SmtpOptions.SectionName)
    .ValidateDataAnnotations()     // validate Required, Range, etc. on startup
    .ValidateOnStart();            // fail fast — validate before first request

// Shorthand (without validation):
builder.Services.Configure<SmtpOptions>(
    builder.Configuration.GetSection(SmtpOptions.SectionName));
Note: ValidateOnStart() validates the options when the host starts — before any request is processed. Without it, options validation (DataAnnotations and custom validators) only runs on first access. ValidateOnStart() means a misconfigured deployment fails loudly at startup rather than silently at runtime when the feature is first used. This is the fail-fast principle applied to configuration — always enable it for required configuration sections in production applications.
Tip: Add a public const string SectionName = "Smtp" constant to your options class. Use it in both the binding call (.BindConfiguration(SmtpOptions.SectionName)) and in tests when constructing the options manually. This eliminates magic strings — if the configuration section is renamed, you change it in one place. The convention is also self-documenting: any developer reading the class can immediately see which configuration section it maps to.
Warning: Do not inject IConfiguration directly into application services to read configuration values — this is the configuration equivalent of using Service Locator. It bypasses the options validation pipeline, makes the service harder to test (you must configure IConfiguration in tests), and hides the service’s configuration requirements. Use IOptions<T> for all configuration in application services. Reserve IConfiguration for infrastructure bootstrapping in Program.cs.

IOptions vs IOptionsSnapshot vs IOptionsMonitor

// ── IOptions<T> — registered as Singleton; value set once at startup ──────
public class EmailService(IOptions<SmtpOptions> options)
{
    private readonly SmtpOptions _smtp = options.Value;
    // Value is fixed for the application lifetime — changes to appsettings
    // require restart. Use for config that should not change at runtime.
    public async Task SendAsync(string to, string subject, string body)
        => Console.WriteLine($"Sending via {_smtp.Host}:{_smtp.Port}");
}

// ── IOptionsSnapshot<T> — registered as Scoped; reloaded per request ──────
public class FeatureService(IOptionsSnapshot<FeatureFlags> flags)
{
    // flags.Value is re-read from configuration on each HTTP request.
    // Changes to appsettings take effect on the NEXT request (no restart).
    // Use for feature flags and runtime-tunable configuration.
    public bool IsNewUiEnabled => flags.Value.NewUi;
}

// ── IOptionsMonitor<T> — registered as Singleton; notified on change ──────
public class RateLimitService(IOptionsMonitor<RateLimitOptions> monitor) : IDisposable
{
    private IDisposable? _subscription;
    private RateLimitOptions _current = monitor.CurrentValue;

    public void StartMonitoring()
    {
        // Callback fires when configuration file is changed
        _subscription = monitor.OnChange(opts =>
        {
            _current = opts;
            Console.WriteLine($"Rate limit updated: {opts.RequestsPerMinute} req/min");
        });
    }
    // Use for long-running services (Singleton, BackgroundService) that
    // need to react to configuration changes without restarting.

    public void Dispose() => _subscription?.Dispose();
}

IOptions Variant Reference

Variant Lifetime Reloads? Best For
IOptions<T> Singleton No (restart needed) Stable config (DB connection, API keys)
IOptionsSnapshot<T> Scoped Per request Feature flags, per-request tuneable config
IOptionsMonitor<T> Singleton On file change Long-running services needing live reload

Common Mistakes

Mistake 1 — Not calling ValidateOnStart (silent misconfiguration)

❌ Wrong — missing SMTP host discovered only when the first email is sent, possibly hours after deployment.

✅ Correct — always chain .ValidateDataAnnotations().ValidateOnStart() for required configuration.

Mistake 2 — Injecting IOptionsSnapshot into a Singleton service (lifetime mismatch)

❌ Wrong — IOptionsSnapshot is Scoped; injecting it into a Singleton throws InvalidOperationException.

✅ Correct — use IOptionsMonitor in Singleton services; use IOptionsSnapshot in Scoped services only.

🧠 Test Yourself

You change a value in appsettings.json at runtime on a running server. Which IOptions variant will pick up the change without restarting the application?