Skip to main content

Command Palette

Search for a command to run...

Mastering the Options Pattern in .NET

Strongly Typed Configuration Made Easy

Updated
5 min read
Mastering the Options Pattern in .NET
M

Morteza Jangjoo, Senior .NET Backend Developer with 15+ years of experience in C#, ASP.NET Core, SQL Server, and Microservices. Skilled in building scalable, high-performance systems.

When building modern applications with .NET, configuration management becomes one of the core design concerns. You don’t want to sprinkle configuration values (like connection strings, API keys, or SMTP credentials) all over your codebase.

Instead, you should use the Options Pattern — a clean, strongly typed, and dependency-injection-friendly way to access configuration values.

Let’s dive deep into how this pattern works and how to apply it in real-world .NET applications. 🚀


What Is the Options Pattern?

The Options Pattern in .NET provides a structured way to represent configuration settings as strongly typed classes that can be easily injected and managed through dependency injection (DI).

Rather than manually reading values using Configuration["Key"], the Options Pattern allows you to bind a section of your appsettings.json file to a strongly typed class.


Example Scenario: SMTP Configuration

Let’s say your appsettings.json contains email configuration values:

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

Step 1 — Define a Strongly Typed Configuration Class

You start by defining a class that matches the shape of your configuration section:

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

Step 2 — Register Configuration with the DI Container

Next, bind your configuration section to the class in Program.cs (for .NET 6 and later):

var builder = WebApplication.CreateBuilder(args);

// Bind the configuration section "SmtpSettings" to the SmtpSettings class
builder.Services.Configure<SmtpSettings>(
    builder.Configuration.GetSection("SmtpSettings")
);

var app = builder.Build();

Now your SmtpSettings values are available throughout the application via dependency injection.


Step 3 — Inject and Use Configuration

You can access configuration values in any service or controller by injecting one of the following interfaces:

  • IOptions<T> — static, reads once at startup

  • IOptionsSnapshot<T> — reloads per request (scoped lifetime)

  • IOptionsMonitor<T> — watches for live changes (singleton)

Let’s explore each one 👇


Option 1: IOptions<T> — Static Configuration

IOptions<T> is the simplest form. It reads configuration values once at startup and never changes during the application's lifetime.

using Microsoft.Extensions.Options;

public class EmailService
{
    private readonly SmtpSettings _smtpSettings;

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

    public void SendEmail()
    {
        Console.WriteLine($"Connecting to SMTP server: {_smtpSettings.Server}:{_smtpSettings.Port}");
    }
}

Best for:
Applications where configuration values remain constant (e.g., static API keys or constants).


Option 2: IOptionsSnapshot<T> — Scoped Configuration (Per Request)

IOptionsSnapshot<T> is a scoped version of options. It reloads configuration automatically on the next HTTP request when the configuration source changes.

This is ideal for ASP.NET Core web apps.

public class EmailController : ControllerBase
{
    private readonly SmtpSettings _smtpSettings;

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

    [HttpGet("send")]
    public IActionResult SendEmail()
    {
        return Ok($"SMTP Server: {_smtpSettings.Server}");
    }
}

Best for:
Web apps where configuration may change occasionally between requests.


Option 3: IOptionsMonitor<T> — Real-time Configuration Changes

IOptionsMonitor<T> supports real-time monitoring of configuration changes.
When the source configuration file (e.g., appsettings.json) changes, .NET automatically triggers an OnChange event.

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

    public EmailBackgroundService(IOptionsMonitor<SmtpSettings> optionsMonitor)
    {
        _optionsMonitor = optionsMonitor;

        // Subscribe to changes
        _optionsMonitor.OnChange(settings =>
        {
            Console.WriteLine($"SMTP settings changed! New server: {settings.Server}");
        });
    }

    public void SendEmail()
    {
        var settings = _optionsMonitor.CurrentValue;
        Console.WriteLine($"Sending email using {settings.Server}");
    }
}

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


⚙️ Summary of Differences

InterfaceLifetimeReloads AutomaticallyTypical Use Case
IOptions<T>Singleton❌ NoStatic configuration
IOptionsSnapshot<T>Scoped✅ Per RequestWeb apps
IOptionsMonitor<T>Singleton✅ Real-timeBackground jobs, services

Named Options — Managing Multiple Configurations

Sometimes you might have multiple configuration sets of the same type, such as Gmail and Outlook SMTP servers.

You can register named options like this:

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

Then retrieve them by name:

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

Advanced Tip — Custom Validation for Options

You can enforce validation rules for your configuration values by using the Validate extension.

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

This ensures that invalid configuration values will throw an error at startup rather than at runtime.


Benefits of the Options Pattern

BenefitDescription
Strongly TypedCompile-time safety for configuration keys
Dependency Injection FriendlyWorks seamlessly with .NET’s DI system
Supports ReloadingIOptionsMonitor and IOptionsSnapshot can refresh automatically
Separation of ConcernsKeeps configuration logic separate from business logic
Easy TestingMocking configuration in unit tests becomes simple

Best Practices

  1. Always use strongly typed configuration classes — avoid Configuration["Key"] directly.

  2. Prefer IOptionsSnapshot in web apps and IOptionsMonitor in background or long-running services.

  3. Use ValidateOnStart() to fail fast if configuration values are invalid.

  4. Keep configuration sections well-organized in appsettings.json.

  5. Never store secrets or passwords in plain text — use User Secrets, Azure Key Vault, or environment variables.


Final Thoughts

The Options Pattern in .NET is a clean, safe, and flexible approach to configuration management.
It allows you to:

  • Centralize your settings,

  • Bind them to strongly typed models,

  • Automatically reload changes,

  • And inject them anywhere using the built-in DI system.

Once you start using it, you’ll never go back to manually accessing configuration values again.

download sample code from github

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