Skip to main content
SEMastery
ASP.NETbeginner

How to Use the Options Pattern in ASP.NET Core 7

A beginner-friendly guide to the options pattern in ASP.NET Core: bind appsettings to classes, and pick between IOptions, IOptionsSnapshot, and IOptionsMonitor.

12 min readUpdated October 3, 2025

A tiffin box packed the night before

Think about a school morning in a busy house. The night before, a parent packs the tiffin box: rice in one section, dal in another, a small sweet in the corner. In the morning nobody is opening jars and digging through the kitchen. The box is ready. You just pick it up and go.

Your ASP.NET Core app has the same need. It uses many small settings: a database connection, an email server name, a page size for lists, an API key. You do not want to dig through configuration with string keys every time you need one. You want a neat box, packed once, ready to grab.

The options pattern is that neatly packed tiffin box. You bind a section of your settings file to a plain C# class one time. After that, any part of your app can ask for that class and get clean, typed values. No string keys. No guessing.

This guide is written for ASP.NET Core 7, but the good news is that the same pattern works without change all the way up to .NET 10. Let us pack the box step by step.

What problem does the options pattern solve?

Before the options pattern, people often read settings like this.

// The messy way — magic strings everywhere
var host = configuration["Email:Host"];
var portText = configuration["Email:Port"];
var port = int.Parse(portText); // hope it is a number!

This works, but it has three real problems. The string "Email:Host" is easy to mistype, and a typo only shows up at runtime as a quiet null. Every value comes back as a string, so you parse by hand. And these strings get sprinkled across many files, so there is no single place that owns the email settings.

The options pattern fixes all three. You describe your settings as a class, bind it once, and inject it. A typo becomes a compile error. Values arrive already typed. And each group of settings lives in exactly one class.

From raw settings to a clean class

appsettings.json
Bind to class
DI container
Your service

Steps

1

appsettings.json

Plain text settings

2

Bind to class

Map section to C#

3

DI container

Register the options

4

Your service

Inject and use

The journey a setting takes from a file to your code.

Step 1: Write your settings in appsettings.json

Let us build a small example: an app that sends emails. Start by adding an Email section to appsettings.json.

{
  "Email": {
    "Host": "smtp.example.com",
    "Port": 587,
    "FromAddress": "[email protected]",
    "UseSsl": true
  }
}

This is the food going into the tiffin box. Each key has a clear name, and the values have natural types: text, a number, and a true/false flag.

Step 2: Make a class that matches the section

Now create a C# class whose property names match the keys in that section. This is the shape of your tiffin box.

public class EmailOptions
{
    public const string SectionName = "Email";
 
    public string Host { get; set; } = string.Empty;
    public int Port { get; set; }
    public string FromAddress { get; set; } = string.Empty;
    public bool UseSsl { get; set; }
}

A few small habits make life easier here. Keeping a SectionName constant means you write the section name once and never mistype it later. Giving string properties a default like string.Empty keeps the compiler happy about nullable warnings. The property names must line up with the JSON keys, because binding matches them by name (and it ignores case, so Host and host both work).

Step 3: Register and bind the options

In Program.cs, tell the dependency injection container to bind that JSON section to your class.

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.Configure<EmailOptions>(
    builder.Configuration.GetSection(EmailOptions.SectionName));
 
var app = builder.Build();

That one Configure call does the packing. It reads the Email section, fills an EmailOptions object, and registers it so the container can hand it out later. From now on, any class can ask for the email settings and get a ready object.

How Configure wires settings into the container at startup

Step 4: Use the options in a service

Here is where the tiffin box gets opened. Inject IOptions<EmailOptions> into any service, and read the typed values from its Value property.

public class EmailSender
{
    private readonly EmailOptions _options;
 
    public EmailSender(IOptions<EmailOptions> options)
    {
        _options = options.Value; // open the box once
    }
 
    public void Send(string to, string body)
    {
        Console.WriteLine(
            $"Connecting to {_options.Host}:{_options.Port} " +
            $"(SSL: {_options.UseSsl}) from {_options.FromAddress}");
        // real send logic would go here
    }
}

Notice there are no string keys and no parsing. _options.Port is already an int. _options.UseSsl is already a bool. Your business code stays clean and never needs to know the settings came from a JSON file. Tomorrow those values could come from environment variables or a cloud secret store, and this class would not change a single line.

The three flavours: IOptions, IOptionsSnapshot, IOptionsMonitor

This is the part most people find confusing, so let us go slowly. There are three ways to ask for your options, and they differ in two things: how long the value lives and whether it can change while the app runs.

Picture three ways of getting the news. IOptions is like a newspaper printed once in the morning. You read the same paper all day. IOptionsSnapshot is like a fresh paper delivered for each visitor who walks in. IOptionsMonitor is like a live news feed that updates itself and can even tap you on the shoulder the second something changes.

The three ways to read options and what makes each different

IOptions — the simple, fast default

IOptions<T> is a singleton. It is built once, the very first time it is needed, and that same value is reused for the whole life of the app. It is the fastest and the simplest. It does not see changes you make to the settings file after the app starts.

Use it for settings that do not change while the app runs, which is honestly most settings: a connection string, a feature flag set at deploy time, an email host. When in doubt, reach for IOptions first.

IOptionsSnapshot — fresh for every request

IOptionsSnapshot<T> is a scoped service. In a web app, that means it is created once per HTTP request. Within a single request the value is steady, but a new request can pick up updated settings if the file changed in between.

Because it is scoped, you cannot inject it into a singleton. ASP.NET Core will throw an error if you try, because a long-lived singleton cannot safely hold a short-lived scoped object. Use IOptionsSnapshot inside normal request handling, like controllers and request-scoped services, when you want each request to see the latest settings.

IOptionsMonitor — always current, even in singletons

IOptionsMonitor<T> is a singleton, but a clever one. You read the current value through its CurrentValue property, and it always gives you the latest. It also has an OnChange callback that fires the moment a setting changes, so you can react in real time.

This is the one to use inside other singletons and inside background services, because those live for the whole app and cannot use the scoped snapshot. Read through CurrentValue each time you need the value, rather than caching it in a field, so you actually benefit from the live updates.

public class HeartbeatService
{
    private readonly IOptionsMonitor<EmailOptions> _monitor;
 
    public HeartbeatService(IOptionsMonitor<EmailOptions> monitor)
    {
        _monitor = monitor;
 
        // run code whenever the settings change
        _monitor.OnChange(updated =>
            Console.WriteLine($"Email host changed to {updated.Host}"));
    }
 
    public void Beat()
    {
        // read CurrentValue each time to stay fresh
        var host = _monitor.CurrentValue.Host;
        Console.WriteLine($"Pinging {host}...");
    }
}

A side-by-side comparison

This table is the one to keep next to you while you decide.

InterfaceLifetimeSees runtime changes?Can go inside a singleton?Best for
IOptions<T>SingletonNoYesSettings fixed at startup
IOptionsSnapshot<T>ScopedYes, once per requestNoControllers and request services
IOptionsMonitor<T>SingletonYes, immediatelyYesSingletons and background jobs

And a quick guide for matching the place in your code to the right choice.

Where you are readingRecommended interfaceWhy
A controller actionIOptionsSnapshot<T>Scoped lifetime fits the request
A regular singleton serviceIOptionsMonitor<T>Stays current without breaking DI rules
A background IHostedServiceIOptionsMonitor<T>Long-lived and needs the latest value
Anything that never changesIOptions<T>Simplest and fastest

How reload works under the hood

You may wonder how a snapshot or monitor "sees" a changed file. The configuration system can watch appsettings.json for edits. When the file changes, the configuration is rebuilt, and the options system rebinds your class. IOptions ignores this because it cached its value forever, but the snapshot and monitor pick it up.

What happens when you edit appsettings.json while running

Edit file
Watcher fires
Rebind options
Snapshot/Monitor updated
IOptions unchanged

Steps

1

Edit file

Save new value

2

Watcher fires

Config reloads

3

Rebind options

Class refilled

4

Snapshot/Monitor updated

New value served

5

IOptions unchanged

Still the old value

The reload path that snapshot and monitor follow.

Here is the same idea as a small sequence, showing two requests around a file edit.

Two requests before and after a settings change

Named options: many boxes of the same shape

Sometimes you need several copies of the same settings class. Imagine two email accounts: one for marketing, one for alerts. They share the EmailOptions shape but hold different values. Named options handle this neatly.

builder.Services.Configure<EmailOptions>(
    "Marketing", builder.Configuration.GetSection("MarketingEmail"));
 
builder.Services.Configure<EmailOptions>(
    "Alerts", builder.Configuration.GetSection("AlertEmail"));

To read a named option, use IOptionsSnapshot or IOptionsMonitor and ask for it by name with the Get method.

public class NotificationService
{
    private readonly EmailOptions _marketing;
    private readonly EmailOptions _alerts;
 
    public NotificationService(IOptionsMonitor<EmailOptions> monitor)
    {
        _marketing = monitor.Get("Marketing");
        _alerts = monitor.Get("Alerts");
    }
}

This is like having two tiffin boxes of the same design, labelled clearly, each packed with different food.

Binding without the section name constant

There is a shorter way to bind that some teams prefer, using BindConfiguration. It does the GetSection and Configure work in one line.

builder.Services.AddOptions<EmailOptions>()
    .BindConfiguration(EmailOptions.SectionName);

This style reads nicely and also gives you a starting point to chain on extra steps later, such as validation. The plain Configure call and this AddOptions call do the same core job, so pick whichever your team finds clearer.

A few good habits

A handful of small choices keep options code tidy and safe over time.

Keep one class per settings section, and keep that class small. A giant options class with thirty unrelated properties is a sign that it should be split. Put the SectionName constant on the class so the section name lives in exactly one place. Inside singletons and background services, read IOptionsMonitor.CurrentValue fresh each time instead of copying it into a field, or you will miss the live updates. And if a setting must never be wrong, add validation so a bad value stops the app at startup with a clear message rather than failing later. Our companion article on adding validation walks through that step by step.

A common mistake to avoid

The single most common error beginners hit is injecting IOptionsSnapshot<T> into a singleton. Because the snapshot is scoped and the singleton is not, ASP.NET Core refuses to build it and throws an error at startup that mentions a scoped service inside a singleton. The fix is simple: in a singleton or a background service, use IOptionsMonitor<T> instead. Keep IOptionsSnapshot<T> for controllers and other request-scoped code.

Quick recap

  • The options pattern binds a section of your settings into a strongly typed class, so you stop using string keys and start using clean properties.
  • Add a section to appsettings.json, make a matching class, register it with Configure or AddOptions().BindConfiguration(...), then inject it.
  • IOptions<T> is a singleton read once at startup. It is the simplest and fastest, and the right default for settings that do not change.
  • IOptionsSnapshot<T> is scoped and gives a fresh value per request. Use it in controllers; never inject it into a singleton.
  • IOptionsMonitor<T> is a singleton that always returns CurrentValue and can react to changes with OnChange. Use it in singletons and background jobs.
  • Named options let you keep several differently-named copies of the same settings class.
  • The same pattern you learned here for ASP.NET Core 7 works unchanged through .NET 10, so this knowledge stays useful for a long time.

References and further reading

Related Posts