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"
}
}
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;
}
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();
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}");
}
}
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}");
}
}
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}");
}
}
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"));
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}");
}
}
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();
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
- Prefer strongly typed options classes over raw
Configurationaccess. - Use
IOptionsSnapshotfor web requests andIOptionsMonitorfor background services. - Add validation using
ValidateOnStart()and data annotations. - Don’t store secrets in appsettings.json — use User Secrets, Azure Key Vault, or environment variables.
- 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)