Mastering the Options Pattern in .NET
Strongly Typed Configuration Made Easy

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 startupIOptionsSnapshot<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
| Interface | Lifetime | Reloads Automatically | Typical Use Case |
IOptions<T> | Singleton | ❌ No | Static configuration |
IOptionsSnapshot<T> | Scoped | ✅ Per Request | Web apps |
IOptionsMonitor<T> | Singleton | ✅ Real-time | Background 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
| Benefit | Description |
| Strongly Typed | Compile-time safety for configuration keys |
| Dependency Injection Friendly | Works seamlessly with .NET’s DI system |
| Supports Reloading | IOptionsMonitor and IOptionsSnapshot can refresh automatically |
| Separation of Concerns | Keeps configuration logic separate from business logic |
| Easy Testing | Mocking configuration in unit tests becomes simple |
Best Practices
Always use strongly typed configuration classes — avoid
Configuration["Key"]directly.Prefer
IOptionsSnapshotin web apps andIOptionsMonitorin background or long-running services.Use
ValidateOnStart()to fail fast if configuration values are invalid.Keep configuration sections well-organized in
appsettings.json.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”




