
Options Pattern in ASP.NET Core Made Simple — Configuration the Right Way
In every ASP.NET Core project, we deal with configuration — whether it’s email settings, external APIs, or feature flags. If you’re still accessing values like Configuration["SmtpSettings:Host"]
throughout your app, it’s time for a better approach.
The Options Pattern in ASP.NET Core offers a cleaner, more maintainable way to handle configuration using strongly typed classes and built-in dependency injection.
In this guide, you’ll learn how to:
- Bind configuration values from
appsettings.json
- Use
IOptions<T>
,IOptionsSnapshot<T>
, andIOptionsMonitor<T>
effectively - Validate them using data annotations
Why Use the Options Pattern?
The Options Pattern is worth using in ASP.NET Core because it gives you strongly typed access to configuration values instead of relying on magic strings, helps you maintain a clean and centralized structure for settings, supports validation using data annotations, works seamlessly with dependency injection, and allows both static and reloadable configuration depending on how you use it.
Add Configuration to appsettings.json
Let’s walk through a real example. Imagine you need to configure SMTP settings to send emails from your application. Instead of hardcoding these values or fetching them manually using configuration keys, you can store them in appsettings.json
and bind them to a strongly typed class using the Options Pattern.
Start by adding a new section called SmtpSettings
in your appsettings.json
file like this:
{
"SmtpSettings": {
"Host": "smtp.example.com",
"Port": 587,
"SenderEmail": "noreply@example.com"
}
}
Create a Strongly Typed Class
Now that you’ve defined the SmtpSettings
section in appsettings.json
, the next step is to create a class that matches its structure.
This class is what ASP.NET Core will use to bind the configuration values — turning them into a strongly typed object that you can safely inject and use throughout your application.
public sealed class SmtpSettings
{
public string Host { get; set; } = string.Empty;
public int Port { get; set; }
public string SenderEmail { get; set; } = string.Empty;
}
Each property name here (like Host
, Port
, and SenderEmail
) matches the keys from your appsettings.json
section. This allows ASP.NET Core to automatically map those values when the application starts.
Register the Configuration in Program.cs
Now that you’ve defined the SmtpSettings class and added values in appsettings.json, the next step is to bind that configuration section to your class so it can be used throughout the application via dependency injection.
You’ll do this in the Program.cs
file by using the Configure() method. This tells ASP.NET Core to bind the "SmtpSettings"
section from your configuration file to the SmtpSettings
class — and make it available through dependency injection using IOptions<T>
.
builder.Services.Configure<SmtpSettings>(
builder.Configuration.GetSection("SmtpSettings"));
Once this is in place, you’ll be able to inject IOptions<T>
into any service or controller and use the values defined in the config file.
Inject and Use the Configuration with IOptions<T>
The simplest way to use the Options Pattern is by injecting IOptions<T>
. This reads the configuration once at application startup, and is suitable for most stable settings.
public sealed class EmailService
{
private readonly SmtpSettings _settings;
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
public void Send()
{
Console.WriteLine($"Sending email from {_settings.SenderEmail} via {_settings.Host}");
}
}
This is ideal when the settings do not need to change during the app’s lifetime — for example, API keys, SMTP credentials, or feature flags.
Other Interfaces: IOptionsSnapshot<T> and IOptionsMonitor<T>
In addition to IOptions, ASP.NET Core provides two more interfaces to handle configuration changes more dynamically.
IOptionsSnapshot<T> — Reloads Per HTTP Request
This interface gives you a fresh copy of the settings on every HTTP request. It’s useful in web APIs or MVC apps where configuration might change frequently.
public sealed class EmailController : ControllerBase
{
private readonly SmtpSettings _settings;
public EmailController(IOptionsSnapshot<SmtpSettings> snapshot)
{
_settings = snapshot.Value;
}
[HttpGet("email-sender")]
public IActionResult GetSender() => Ok(_settings.SenderEmail);
}
- Scoped per request
- Reloads values if reloadOnChange: true is set in configuration
- Not suitable for singleton or background services
IOptionsMonitor<T> — Real-Time Updates + Change Events
This is the most powerful interface. It supports real-time updates to config values and allows you to react to changes using callbacks.
public sealed class EmailWorker
{
private readonly IOptionsMonitor<SmtpSettings> _monitor;
public EmailWorker(IOptionsMonitor<SmtpSettings> monitor)
{
_monitor = monitor;
_monitor.OnChange(updated =>
{
Console.WriteLine($"SMTP host changed to: {updated.Host}");
});
}
public void DoWork()
{
var current = _monitor.CurrentValue;
Console.WriteLine($"Current sender: {current.SenderEmail}");
}
}
- Supports .CurrentValue to access the latest settings
- Use .OnChange() to subscribe to runtime config updates
- Ideal for background services or long-running processes
Add Validation Using Data Annotations
To make your configuration more reliable and error-proof, ASP.NET Core allows you to validate settings using data annotations. These are the same attributes you might already be using for model validation — like [Required]
, [Range]
, and [EmailAddress]
.
By adding them to your settings class, you can ensure that your configuration values are not just present but also valid. This helps prevent subtle bugs caused by missing or incorrectly formatted values.
Here’s how you can apply it to the SmtpSettings
class:
using System.ComponentModel.DataAnnotations;
public sealed class SmtpSettings
{
[Required]
public string Host { get; set; } = string.Empty;
[Range(1, 65535)]
public int Port { get; set; }
[Required, EmailAddress]
public string SenderEmail { get; set; } = string.Empty;
}
In this example:
- Host must not be null or empty
- Port must be between 1 and 65535
- SenderEmail must be a valid email address
Once you’ve defined these rules, update your Program.cs
to enable validation at startup:
builder.Services
.AddOptions<SmtpSettings>()
.Bind(builder.Configuration.GetSection("SmtpSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
This setup ensures your app fails fast if configuration is invalid — so issues can be caught early, before they cause problems in production.
Summary
The Options Pattern in ASP.NET Core gives us a clean and type-safe way to access configuration values throughout our application. Instead of relying on magic strings or manual parsing, we can bind settings directly to strongly typed classes and inject them where needed.
For most scenarios, configuring options using IConfiguration
and AddOptions
is simple and effective. If additional customization is needed, more advanced patterns like IConfigureOptions
are also available.
When it comes to consuming configuration, there are three interfaces to choose from:
- IOptions — great for static, unchanging config
- IOptionsSnapshot — perfect for per-request reload in web apps
- IOptionsMonitor — best when real-time updates or change tracking is needed
Choosing the right one depends on the behavior your application requires. For many cases, IOptions<T>
is more than enough. But when runtime flexibility matters, the other two interfaces offer powerful alternatives.
Quick Takeaway
The Options Pattern has made my configuration setup way easier — especially with validation and real-time updates. If you’re still using Configuration["Key"]
, it’s a great time to switch to this cleaner, more scalable approach.