The Options Pattern
🎯 What You'll Learn
- What the Options Pattern is and why use it
- Creating strongly-typed configuration classes
- Registering options in Program.cs
- Injecting options with IOptions, IOptionsSnapshot, IOptionsMonitor
- Validating configuration with Data Annotations
- Named options for multiple configurations
- Post-configuration and validation
- Best practices for InvenTrack options
What is the Options Pattern?
The Options Pattern is the recommended way to access configuration in
ASP.NET Core. Instead of reading strings from IConfiguration, you bind
configuration sections to strongly-typed classes.
Type Safety: Compile-time checking instead of runtime errors
IntelliSense: Auto-completion for configuration properties
Validation: Validate configuration at startup
Testability: Easy to mock in unit tests
Separation of Concerns: Configuration logic separate from business logic
Basic Usage
Step 1: Create an Options Class
namespace InvenTrack.Api.Options;
public class JwtSettings
{
public string Secret { get; set; } = string.Empty;
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; }
}
Step 2: Configure in appsettings.json
{
"JwtSettings": {
"Secret": "your-secret-key-here-min-32-chars",
"Issuer": "InvenTrack.Api",
"Audience": "InvenTrack.Client",
"ExpirationMinutes": 60
}
}
Step 3: Register Options in Program.cs
builder.Services.Configure<JwtSettings>(
builder.Configuration.GetSection("JwtSettings"));
Step 4: Inject and Use
using Microsoft.Extensions.Options;
public class AuthService
{
private readonly JwtSettings _jwtSettings;
public AuthService(IOptions<JwtSettings> jwtOptions)
{
_jwtSettings = jwtOptions.Value;
}
public string GenerateToken(string userId)
{
// Use _jwtSettings.Secret, _jwtSettings.Issuer, etc.
var key = Encoding.UTF8.GetBytes(_jwtSettings.Secret);
// ... token generation logic
}
}
IOptions vs IOptionsSnapshot vs IOptionsMonitor
There are three interfaces for accessing options:
IOptions<T>
public MyService(IOptions<JwtSettings> options)
{
var settings = options.Value;
}
- Lifetime: Singleton
- Reloading: Does NOT reload when config changes
- Use when: Configuration doesn't change during runtime
- Best for: Most scenarios
IOptionsSnapshot<T>
public MyService(IOptionsSnapshot<JwtSettings> options)
{
var settings = options.Value;
}
- Lifetime: Scoped (per request)
- Reloading: Reloads on each request
- Use when: Config might change between requests
- Best for: Named options, per-request config
IOptionsMonitor<T>
public MyService(IOptionsMonitor<JwtSettings> options)
{
var settings = options.CurrentValue;
// Subscribe to changes
options.OnChange(newSettings =>
{
Console.WriteLine("Configuration changed!");
});
}
- Lifetime: Singleton
- Reloading: Reloads immediately when config changes
- Use when: Need to react to config changes in real-time
- Best for: Singleton services that need live updates
| Feature | IOptions | IOptionsSnapshot | IOptionsMonitor |
|---|---|---|---|
| Lifetime | Singleton | Scoped | Singleton |
| Reloads | ❌ No | ✅ Per request | ✅ Immediately |
| Named options | ❌ No | ✅ Yes | ✅ Yes |
| Change notifications | ❌ No | ❌ No | ✅ Yes |
Default choice: Use IOptions<T> for most scenarios
Per-request config: Use IOptionsSnapshot<T>
Real-time updates: Use IOptionsMonitor<T>
Validation with Data Annotations
You can validate options using Data Annotations:
using System.ComponentModel.DataAnnotations;
public class JwtSettings
{
[Required(ErrorMessage = "JWT Secret is required")]
[MinLength(32, ErrorMessage = "JWT Secret must be at least 32 characters")]
public string Secret { get; set; } = string.Empty;
[Required]
public string Issuer { get; set; } = string.Empty;
[Required]
public string Audience { get; set; } = string.Empty;
[Range(1, 1440, ErrorMessage = "Expiration must be between 1 and 1440 minutes")]
public int ExpirationMinutes { get; set; }
}
Enable Validation
builder.Services.AddOptions<JwtSettings>()
.Bind(builder.Configuration.GetSection("JwtSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
ValidateDataAnnotations()- Enables Data Annotations validationValidateOnStart()- Validates at startup (fails fast)
Without ValidateOnStart(), validation only happens when options are first
accessed. Use ValidateOnStart() to catch configuration errors at startup!
Custom Validation
For complex validation, implement IValidateOptions<T>:
using Microsoft.Extensions.Options;
public class JwtSettingsValidator : IValidateOptions<JwtSettings>
{
public ValidateOptionsResult Validate(string? name, JwtSettings options)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(options.Secret))
{
errors.Add("JWT Secret is required");
}
else if (options.Secret.Length < 32)
{
errors.Add("JWT Secret must be at least 32 characters");
}
if (options.ExpirationMinutes < 1 || options.ExpirationMinutes > 1440)
{
errors.Add("Expiration must be between 1 and 1440 minutes");
}
return errors.Count == 0
? ValidateOptionsResult.Success
: ValidateOptionsResult.Fail(errors);
}
}
Register Custom Validator
builder.Services.AddSingleton<IValidateOptions<JwtSettings>, JwtSettingsValidator>();
builder.Services.AddOptions<JwtSettings>()
.Bind(builder.Configuration.GetSection("JwtSettings"))
.ValidateOnStart();
Post-Configuration
You can modify options after they're bound from configuration:
builder.Services.PostConfigure<JwtSettings>(options =>
{
// Ensure Secret is trimmed
options.Secret = options.Secret.Trim();
// Set default if not provided
if (options.ExpirationMinutes == 0)
{
options.ExpirationMinutes = 60;
}
});
Named Options
You can have multiple configurations of the same type:
{
"EmailSettings": {
"Transactional": {
"SmtpServer": "smtp.sendgrid.net",
"SmtpPort": 587
},
"Marketing": {
"SmtpServer": "smtp.mailchimp.com",
"SmtpPort": 587
}
}
}
builder.Services.Configure<EmailSettings>("Transactional",
builder.Configuration.GetSection("EmailSettings:Transactional"));
builder.Services.Configure<EmailSettings>("Marketing",
builder.Configuration.GetSection("EmailSettings:Marketing"));
public class EmailService
{
private readonly EmailSettings _transactionalSettings;
private readonly EmailSettings _marketingSettings;
public EmailService(IOptionsSnapshot<EmailSettings> emailOptions)
{
_transactionalSettings = emailOptions.Get("Transactional");
_marketingSettings = emailOptions.Get("Marketing");
}
}
Complete InvenTrack Example
Options Classes
using System.ComponentModel.DataAnnotations;
namespace InvenTrack.Api.Options;
public class InventorySettings
{
[Range(1, 1000)]
public int LowStockThreshold { get; set; } = 10;
public bool EnableAutoReorder { get; set; }
[Required]
[RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Currency must be 3 uppercase letters")]
public string DefaultCurrency { get; set; } = "USD";
}
using System.ComponentModel.DataAnnotations;
namespace InvenTrack.Api.Options;
public class EmailSettings
{
[Required]
public string SmtpServer { get; set; } = string.Empty;
[Range(1, 65535)]
public int SmtpPort { get; set; } = 587;
[Required]
[EmailAddress]
public string SenderEmail { get; set; } = string.Empty;
[Required]
public string SenderName { get; set; } = string.Empty;
public bool EnableSsl { get; set; } = true;
}
Registration in Program.cs
// Register all options with validation
builder.Services.AddOptions<JwtSettings>()
.Bind(builder.Configuration.GetSection("JwtSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<InventorySettings>()
.Bind(builder.Configuration.GetSection("InventorySettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddOptions<EmailSettings>()
.Bind(builder.Configuration.GetSection("EmailSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
Usage in Services
public class InventoryService
{
private readonly InventorySettings _settings;
public InventoryService(IOptions<InventorySettings> options)
{
_settings = options.Value;
}
public bool IsLowStock(int quantity)
{
return quantity < _settings.LowStockThreshold;
}
}
Key Takeaways
- The Options Pattern provides strongly-typed configuration
- Create POCO classes to represent configuration sections
- Register with
builder.Services.Configure<T>() - IOptions<T> - Singleton, no reloading (most common)
- IOptionsSnapshot<T> - Scoped, reloads per request
- IOptionsMonitor<T> - Singleton, immediate reloading
- Use Data Annotations for simple validation
- Implement IValidateOptions<T> for complex validation
ValidateOnStart()catches errors at startupPostConfigure()modifies options after binding- Named options for multiple configurations of same type
- Always validate critical configuration
You now understand the Options Pattern for strongly-typed, validated configuration! In the final section of Part III, we'll explore Environments—how to manage Development, Staging, and Production environments, and how to configure your application differently for each.