The Options Pattern in C#
Configuration in .NET applications typically lives in appsettings.json, environment variables, or user secrets. Getting that configuration data out of a file and into your classes in a clean, testable way is what the options pattern solves. Instead of manually reading JSON keys or passing raw strings around, you bind a configuration section to a strongly-typed C# class and let dependency injection deliver it wherever it's needed.
In his video "The Options Pattern in C#", Tim Corey introduces the three variations of the options pattern (IOptions, IOptionsSnapshot, and IOptionsMonitor), demonstrates each one in a Blazor server app, and shows how to add validation so your application fails fast when configuration is missing or malformed. If you're building any .NET project that reads from configuration files and uses dependency injection, this pattern is foundational.
The Setup: A POCO Model and appsettings.json
[0:34 - 1:46] Tim starts with a Blazor web app (server-side rendered, no client-side interactivity) that already has two pieces in place. The first is a section in appsettings.json with three key-value pairs under a CloudInfo section:
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}The second piece is a plain C# class (a POCO) whose properties mirror the JSON keys:
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}Tim notes that the configuration source doesn't have to be appsettings.json specifically. It could be appsettings.Development.json, secrets.json, or any combination. The options pattern reads from whatever configuration providers are registered in the application, and the property names on the POCO match the JSON keys by convention.
Registering Options in Program.cs
[2:25 - 3:15] Wiring the configuration to dependency injection takes two method calls in Program.cs:
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");AddOptions<T>() registers the type with DI. BindConfiguration("CloudInfo") tells the framework which section of the configuration to map. The string "CloudInfo" corresponds to the JSON key in appsettings.json. If the section name and the class name don't match, the string argument is what the framework uses to find the right data.
IOptions: The Singleton Approach
[3:15 - 5:18] With the registration in place, Tim injects the configuration into a Blazor page using IOptions<CloudInfoOptions>:
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}The critical detail is that IOptions<T> is registered as a singleton. The configuration values are read once when the application starts and cached for the lifetime of the process. If you change appsettings.json while the app is running, IOptions will not reflect those changes.
For most applications, this is the right choice. Configuration rarely changes at runtime, and singleton lifetime means zero overhead per request.
IOptionsSnapshot: Scoped Reloading
[5:18 - 6:55] Tim swaps IOptions for IOptionsSnapshot to demonstrate the scoped variant:
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig@inject IOptionsSnapshot<CloudInfoOptions> CloudConfigIOptionsSnapshot<T> reads the configuration fresh for each request (scoped lifetime). If you modify appsettings.json while the app is running, the next web request will pick up the new values. The previous request keeps its own snapshot, so there's no mid-request inconsistency.
This is useful for applications where configuration changes need to take effect without a restart, such as toggling feature flags, updating API endpoints, or rotating storage connection strings. The trade-off is a small per-request cost to re-read the configuration, which is negligible for most workloads.
IOptionsMonitor: Live Change Notifications
[6:55 - 8:34] The third variant, IOptionsMonitor<T>, goes further than snapshot reloading. It actively watches for configuration changes and can trigger callbacks when values update:
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}Unlike IOptionsSnapshot, which only refreshes between requests, IOptionsMonitor can detect changes during a long-running operation. Tim points out that this is most relevant for background services, SignalR hubs, or any component with a longer lifetime than a single HTTP request.
Adding Validation
[8:34 - 10:05] The final piece Tim covers is validation. The options pattern supports inline validation rules that run when the configuration is first accessed:
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));Tim demonstrates the failure case by removing the Storage value from appsettings.json and launching the application. The result is an immediate unhandled exception: "CloudInfoOptions failed validation." The application refuses to start with incomplete configuration, which is exactly the behavior you want. Discovering a missing value at startup is far better than hitting a null reference in production at 2 AM.
For more complex validation scenarios (cross-property checks, conditional rules), you can chain multiple .Validate() calls or implement IValidateOptions<T> as a dedicated validation class.
Wrapping Up: Three Interfaces, One Pattern
[10:05 - 10:10] The options pattern provides a clean separation between configuration storage and consumption. IOptions covers the majority of use cases where configuration is static. IOptionsSnapshot handles applications that need to pick up changes between requests. IOptionsMonitor serves long-running components that need real-time awareness of configuration updates. And validation ensures your application fails loudly when required values are missing.
Conclusion
[10:10 - 10:15] To sum it up: register your configuration with AddOptions<T>().BindConfiguration("SectionName") in Program.cs, inject one of the three interfaces (IOptions, IOptionsSnapshot, or IOptionsMonitor) depending on your reload requirements, and add .Validate() to catch missing values at startup instead of at runtime.
The pattern works in any .NET project type: Blazor, ASP.NET Core, console apps, worker services. The registration is the same everywhere.
Example Tip: If you're unsure which interface to use, start with IOptions<T>. It's the simplest, has zero per-request overhead, and covers the vast majority of cases where configuration is set at startup and doesn't change. Only move to IOptionsSnapshot or IOptionsMonitor when you have a concrete need for runtime reloading.
Watch full video on his YouTube Channel and gain more insights on C# configuration patterns.

