DEV Community

Cover image for Mastering the Options Pattern in .NET — Strongly Typed Configuration Made Simple
Morteza Jangjoo
Morteza Jangjoo

Posted on

Mastering the Options Pattern in .NET — Strongly Typed Configuration Made Simple

When building modern applications with .NET, configuration management is one of the most critical design aspects.

Instead of spreading configuration keys like API URLs or SMTP credentials across your codebase, .NET provides a clean, strongly typed, and dependency-injection-friendly solution: the Options Pattern.

Let’s explore this pattern in detail and see how to use it properly in real-world applications.


What Is the Options Pattern?

The Options Pattern allows you to represent configuration settings as strongly typed classes that can be easily injected and validated.

Instead of calling Configuration["SomeKey"], you can map a JSON configuration section directly to a C# class.


Example Scenario — SMTP Settings

Here’s an example appsettings.json configuration:

{
  "SmtpSettings": {
    "Server": "smtp.gmail.com",
    "Port": 587,
    "Username": "myemail@gmail.com",
    "Password": "mypassword"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 1 — Define a Strongly Typed Configuration Class

public class SmtpSettings
{
    public string Server { get; set; } = string.Empty;
    public int Port { get; set; }
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Register Configuration in Program.cs

In .NET 6+:

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<SmtpSettings>(
    builder.Configuration.GetSection("SmtpSettings")
);

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

Now your configuration is available through the DI system.


Step 3 — Access Configuration via Dependency Injection

There are three interfaces to access Options in .NET:

  • IOptions<T> — static, read once at startup
  • IOptionsSnapshot<T> — reloads per request (scoped)
  • IOptionsMonitor<T> — observes live changes (singleton)

Let’s break them down


IOptions<T> — Static Configuration

using Microsoft.Extensions.Options;

public class EmailService
{
    private readonly SmtpSettings _settings;

    public EmailService(IOptions<SmtpSettings> options)
    {
        _settings = options.Value;
    }

    public void SendEmail()
    {
        Console.WriteLine($"Using SMTP server: {_settings.Server}:{_settings.Port}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Best for static configuration that never changes during the app’s lifetime.


IOptionsSnapshot<T> — Per Request Reload

IOptionsSnapshot reloads values automatically for each web request, making it ideal for ASP.NET Core applications.

public class EmailController : ControllerBase
{
    private readonly SmtpSettings _settings;

    public EmailController(IOptionsSnapshot<SmtpSettings> options)
    {
        _settings = options.Value;
    }

    [HttpGet("send")]
    public IActionResult SendEmail()
    {
        return Ok($"SMTP Server: {_settings.Server}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Use this in web apps where configuration might change between requests.


IOptionsMonitor<T> — Real-time Change Notifications

IOptionsMonitor provides live reload and change notification support.

public class EmailBackgroundService
{
    private readonly IOptionsMonitor<SmtpSettings> _monitor;

    public EmailBackgroundService(IOptionsMonitor<SmtpSettings> monitor)
    {
        _monitor = monitor;

        _monitor.OnChange(settings =>
        {
            Console.WriteLine($"SMTP settings changed! New server: {settings.Server}");
        });
    }

    public void SendEmail()
    {
        var settings = _monitor.CurrentValue;
        Console.WriteLine($"Sending email via {settings.Server}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Best for background services or long-running processes that must react immediately to configuration updates.


Options Interface Comparison

Interface Lifetime Auto Reload Ideal Use Case
IOptions<T> Singleton ❌ No Static configuration
IOptionsSnapshot<T> Scoped ✅ Per Request ASP.NET Core apps
IOptionsMonitor<T> Singleton ✅ Real-time Background jobs, daemons

Named Options — Multiple Configurations

You can manage multiple sets of configurations (for example, Gmail and Outlook) using named options:

builder.Services.Configure<SmtpSettings>("Gmail", builder.Configuration.GetSection("SmtpGmail"));
builder.Services.Configure<SmtpSettings>("Outlook", builder.Configuration.GetSection("SmtpOutlook"));
Enter fullscreen mode Exit fullscreen mode

Then:

public class EmailService
{
    private readonly IOptionsSnapshot<SmtpSettings> _options;

    public EmailService(IOptionsSnapshot<SmtpSettings> options)
    {
        _options = options;
    }

    public void SendViaGmail()
    {
        var gmail = _options.Get("Gmail");
        Console.WriteLine($"Sending via Gmail: {gmail.Server}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Validating Options

It’s good practice to validate configuration values during startup.

builder.Services
    .AddOptions<SmtpSettings>()
    .Bind(builder.Configuration.GetSection("SmtpSettings"))
    .Validate(settings => settings.Port > 0, "Port must be greater than 0")
    .ValidateDataAnnotations()
    .ValidateOnStart();
Enter fullscreen mode Exit fullscreen mode

This ensures invalid configuration will throw an exception at startup, not at runtime.


Benefits of Using the Options Pattern

Benefit Description
Strongly Typed Compile-time safety for configuration keys
DI Integration Works naturally with .NET’s dependency injection
Supports Reload IOptionsMonitor and IOptionsSnapshot can refresh automatically
Separation of Concerns Keeps configuration separate from business logic
Testable Easy to mock in unit tests

Best Practices

  1. Prefer strongly typed options classes over raw Configuration access.
  2. Use IOptionsSnapshot for web requests and IOptionsMonitor for background services.
  3. Add validation using ValidateOnStart() and data annotations.
  4. Don’t store secrets in appsettings.json — use User Secrets, Azure Key Vault, or environment variables.
  5. Organize your configuration sections logically (one class per section).

Final Thoughts

The Options Pattern in .NET provides a robust, clean, and scalable approach to configuration management.
It simplifies how you handle settings, supports automatic reloads, and integrates perfectly with .NET’s dependency injection system.

Once you adopt it, configuration management becomes safer, more maintainable, and more professional.

download sample code from github

I’m Morteza Jangjoo and “Explaining things I wish someone had explained to me”

Top comments (0)