How to Use Options Pattern in C# for Typed Settings

The Options pattern in .NET provides a strongly typed approach to managing application settings, replacing brittle string-based configuration keys with structured C# objects. This article explains how to define settings classes, bind them to configuration sources such as appsettings.json, register them within the Dependency Injection (DI) container, and consume them effectively across your application using IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T>.

1. Define the Settings Model

Create a Plain Old CLR Object (POCO) that mirrors the structure of your configuration section.

public class EmailSettings
{
    public const string SectionName = "EmailSettings";

    public string SmtpServer { get; set; } = string.Empty;
    public int Port { get; set; }
    public string SenderEmail { get; set; } = string.Empty;
    public bool EnableSsl { get; set; }
}

2. Define Configuration in appsettings.json

Add the corresponding section to your appsettings.json file. Ensure the property names match the properties in your C# class.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "SenderEmail": "noreply@example.com",
    "EnableSsl": true
  }
}

3. Register the Options in the DI Container

In modern .NET applications (Program.cs), bind the configuration section to your options class using the Configure method.

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();

4. Inject and Use the Settings

Inject the options into your services or controllers via constructor injection. The base interface is IOptions<T>.

using Microsoft.Extensions.Options;

public class EmailService
{
    private readonly EmailSettings _settings;

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

    public void SendEmail(string recipient, string subject, string body)
    {
        var smtpHost = _settings.SmtpServer;
        var port = _settings.Port;
        // Logic to send email
    }
}

5. Choosing the Right Options Interface

The .NET runtime offers three interfaces for consuming options, depending on lifetime and dynamic reload requirements:

  • IOptions<T>:
    • Lifetime: Singleton.
    • Behavior: Reads configuration once at application startup. It does not update if configuration changes occur while the app is running.
    • Best for: Settings that never change without restarting the app.
  • IOptionsSnapshot<T>:
    • Lifetime: Scoped.
    • Behavior: Recomputed once per HTTP request or scope. Reflects changes in external configuration files without restarting the app. Cannot be injected into Singleton services.
    • Best for: Web applications needing fresh settings per request.
  • IOptionsMonitor<T>:
    • Lifetime: Singleton.
    • Behavior: Provides real-time notifications when configuration values change via the CurrentValue property and an OnChange callback. Can be safely injected into Singleton services.
    • Best for: Background services or Singletons that need real-time configuration reload.

Example Using IOptionsSnapshot<T>

public class NotificationController : ControllerBase
{
    private readonly EmailSettings _settings;

    public NotificationController(IOptionsSnapshot<EmailSettings> options)
    {
        _settings = options.Value; // Updated if appsettings.json changes
    }
}

6. Adding Validation to Settings

To prevent the application from starting with invalid or missing settings, enable data validation using data annotations.

Update the model:

using System.ComponentModel.DataAnnotations;

public class EmailSettings
{
    public const string SectionName = "EmailSettings";

    [Required]
    public string SmtpServer { get; set; } = string.Empty;

    [Range(1, 65535)]
    public int Port { get; set; }

    [EmailAddress]
    public string SenderEmail { get; set; } = string.Empty;
}

Register validation in Program.cs:

builder.Services.AddOptions<EmailSettings>()
    .Bind(builder.Configuration.GetSection(EmailSettings.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Ensures validation occurs at app startup rather than first access