4 min readBy Muhammad Shahid
IOptions vs IOptionsSnapshot vs IOptionsMonitor in ASP.NET Core
When to inject IOptions, IOptionsSnapshot, or IOptionsMonitor — lifetimes, reload, named options, and the production bugs I see when teams pick the wrong one.
Part of Dependency Injection
Teams search IOptions vs IOptionsSnapshot vs IOptionsMonitor after a setting change in Azure does nothing, or after a singleton starts serving the wrong clinic’s feature flag. The three interfaces look interchangeable in a tutorial. They are not.
This sits next to the config file guide. That page is which JSON files exist. This page is how you consume them in C# without lying about lifetime.
The one-line difference
| Type | Lifetime of the wrapper | Sees file / Azure reloads? | Typical inject site |
|---|---|---|---|
IOptions<T> | Singleton | No — value from first bind | Singletons that must not change mid-process |
IOptionsSnapshot<T> | Scoped | Yes, per request | Controllers, handlers, request services |
IOptionsMonitor<T> | Singleton | Yes, immediately | Background services, caches, long-lived workers |
T is your options class. Bind it once:
builder.Services.Configure<IdentityServerOptions>(
builder.Configuration.GetSection("IdentityServer"));
Then pick the interface to match who lives how long, not which name you memorized.
IOptions<T> — snapshot at startup
IOptions<T>.Value is resolved once and cached. Change IdentityServer__Authority in App Service and recycle-less reload will not update a singleton that captured IOptions.
I use it when the value is a process constant: signing algorithm name, a feature that requires a restart anyway, or a library that only accepts a POCO at construction.
Do not inject IOptions<T> into a singleton and then wonder why portal edits did nothing. That is not a config bug. That is the contract.
IOptionsSnapshot<T> — once per request
Scoped. Each HTTP request gets a fresh bind if the configuration provider reloaded. Controllers and MediatR handlers can read the current Azure setting without holding it for the life of the process.
public sealed class FeeScheduleHandler
{
private readonly FeeOptions _options;
public FeeScheduleHandler(IOptionsSnapshot<FeeOptions> options)
{
_options = options.Value;
}
}
Do not inject IOptionsSnapshot<T> into a Singleton. That is a captive dependency — the same class of bug as a singleton DbContext. The container will refuse it in recent .NET, or worse, give you one scoped snapshot forever.
IOptionsMonitor<T> — live updates in a singleton
BackgroundService, a hosted refresh worker, a singleton cache: they cannot take IOptionsSnapshot. They take IOptionsMonitor<T>.
public sealed class ReportCache : IDisposable
{
private readonly IDisposable? _onChange;
public ReportCache(IOptionsMonitor<CacheOptions> monitor)
{
Apply(monitor.CurrentValue);
_onChange = monitor.OnChange(Apply);
}
private void Apply(CacheOptions options) { /* resize, bump version */ }
public void Dispose() => _onChange?.Dispose();
}
CurrentValue is the latest. OnChange fires when JSON or environment reload. Unsubscribe in Dispose or you leak.
On healthcare fee-schedule publishes I still prefer an explicit version stamp in the cache key over hoping OnChange ran. Monitor is for “ops flipped a flag.” Product data still needs a publish event.
Named options
IOptionsSnapshot<T>.Get("SellerPortal") and IOptionsMonitor<T>.Get("SellerPortal") are how you bind two sections to one type. IOptions<T> is the default (unnamed) instance only. If you have buyer vs seller JWT settings, use named options — not two nearly identical classes.
Validate on start
A missing IdentityServer:Authority should fail the process at boot, not on the first Angular login. I add:
builder.Services.AddOptions<IdentityServerOptions>()
.BindConfiguration("IdentityServer")
.ValidateDataAnnotations()
.ValidateOnStart();
That is not a substitute for Key Vault. It is a substitute for “200 on /health and 500 on /connect.” File names and Azure overlays stay in the config file guide. DI lifetimes stay in DI lifetimes. This page is which options interface you inject.
Checklist
- Options class has a parameterless constructor and settable properties (bind requirement)
- Section name matches Azure
__keys - Request code uses
IOptionsSnapshot<T> - Singletons / hosted services use
IOptionsMonitor<T>or startup-onlyIOptions<T> - No
IOptionsSnapshot<T>in a Singleton -
OnChangeis disposed
If a setting works after recycle and fails after a portal edit, you almost always injected IOptions<T> into a long-lived service. Swap to monitor, or accept that this setting requires a restart and document it.
If you want a second pair of eyes on options lifetimes for an ASP.NET Core API, contact me.