How to Manage Application Settings Using the Options Pattern in .NET

Learn how to manage application configuration in .NET using the Options Pattern, including strongly typed settings, IOptions, IOptionsSnapshot, and IOptionsMonitor.

How to Manage Application Settings Using the Options Pattern in .NET cover

Application settings are used in almost every application. Things like API URLs, connection strings, feature flags, timeout values, and other environment-specific values usually shouldn't be hardcoded directly into the application.

In .NET, we can store these values in appsettings.json and access them using IConfiguration. This works well, especially when we only have a few settings. But as the application grows, we may end up reading many configuration values in different places using string-based keys.

The Options Pattern provides a cleaner way to handle related configuration settings. We can represent a configuration section using a strongly typed class and then inject that configuration into the services that need it.

In this article, we'll start with a simple example using IConfiguration, see where it becomes inconvenient, and then use the Options Pattern to make the same configuration easier to work with. We will also look at IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> and understand how they differ.

The Problem with Reading Settings Directly

Let's take a simple example where our application has a few settings related to how it behaves:

{ "AppSettings": { "ApplicationName": "MyApplication", "MaxItems": 100, "EnableLogging": true } }

We can read these values directly using IConfiguration:

public class ProductService { private readonly IConfiguration _configuration; public ProductService(IConfiguration configuration) { _configuration = configuration; } public void Process() { var applicationName = _configuration["AppSettings:ApplicationName"]; var maxItems = _configuration.GetValue<int>("AppSettings:MaxItems"); var enableLogging = _configuration.GetValue<bool>("AppSettings:EnableLogging"); // Application logic } }

There is nothing wrong with this approach. In fact, IConfiguration is exactly what .NET provides for reading configuration values. The problem becomes more noticeable when we have many related settings and need to use them in multiple places.

For example, every time we need a value, we have to know its configuration key. These keys are strings, so a small typo won't be caught by the compiler. We may also end up repeating the same configuration keys across different services, making the code harder to maintain as the application grows.

Another issue is that the service doesn't clearly show which configuration values it actually depends on. From the constructor, we only know that it receives IConfiguration; we have to look through the implementation to find which settings are being used.

Instead of accessing each setting individually, we can represent the entire AppSettings section with a C# class. This gives us strongly typed properties and keeps the configuration structure in one place.

That's the problem the Options Pattern helps us solve.

Introducing the Options Pattern

To make the configuration strongly typed, we can create a class that represents the AppSettings section from our appsettings.json file.

public class AppSettings { public string ApplicationName { get; set; } = string.Empty; public int MaxItems { get; set; } public bool EnableLogging { get; set; } }

The property names correspond to the settings we already have in appsettings.json:

{ "AppSettings": { "ApplicationName": "MyApplication", "MaxItems": 100, "EnableLogging": true } }

Now we need to tell .NET that the AppSettings configuration section should be bound to our AppSettings class. We can do that in Program.cs using Configure<T>().

builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

GetSection("AppSettings") selects the AppSettings section from the configuration, while Configure<AppSettings>() registers that section so it can be consumed as a strongly typed object.

Once it is registered, we can inject IOptions<AppSettings> into a service:

public class ProductService { private readonly AppSettings _settings; public ProductService(IOptions<AppSettings> options) { _settings = options.Value; } public void Process() { Console.WriteLine(_settings.ApplicationName); Console.WriteLine(_settings.MaxItems); if (_settings.EnableLogging) { Console.WriteLine("Logging is enabled."); } } }

Now the service no longer needs to know configuration keys such as "AppSettings:MaxItems". It works with normal C# properties instead.

This also makes the relationship between the configuration and the code much clearer. If we look at AppSettings, we can immediately see which values belong to that configuration section, and the compiler can help us when working with those properties.

At this point, we have converted a configuration section into a strongly typed object. But IOptions<T> is only one way of consuming configuration. The important question is what happens when configuration values can change while the application is running.

IOptions<T>, IOptionsSnapshot<T> and IOptionsMonitor<T>

So far, we have used IOptions<AppSettings> to access our configuration. For many applications, this is all we need. But the three options interfaces are not interchangeable. They differ mainly in how they handle the lifetime and changes of configuration values.

IOptions<T>

IOptions<T> is the simplest option and is suitable when configuration values are not expected to change while the application is running.

public class ProductService { private readonly IOptions<AppSettings> _options; public ProductService(IOptions<AppSettings> options) { _options = options; } public void Process() { var maxItems = _options.Value.MaxItems; } }

The value accessed through IOptions<T> represents the configuration available when the application is running. If the underlying configuration source changes later, IOptions<T> does not automatically provide the updated value.

This makes IOptions<T> a good choice for normal application settings such as a fixed application name, default limits, or other values that are not expected to change at runtime.

IOptionsSnapshot<T>

IOptionsSnapshot<T> is useful when we want to pick up configuration changes without restarting the application. It is registered as a scoped service, so a new snapshot is created for each request in a typical ASP.NET Core application.

public class ProductService { private readonly IOptionsSnapshot<AppSettings> _options; public ProductService(IOptionsSnapshot<AppSettings> options) { _options = options; } public void Process() { var maxItems = _options.Value.MaxItems; } }

If the configuration changes, a new request can receive the updated value. However, the value remains consistent throughout that particular request.

This makes IOptionsSnapshot<T> useful when configuration can change and we want those changes to be picked up on subsequent requests.

IOptionsMonitor<T>

IOptionsMonitor<T> is designed for scenarios where we need to access the current configuration value and potentially react when that value changes.

public class ProductService { private readonly IOptionsMonitor<AppSettings> _options; public ProductService(IOptionsMonitor<AppSettings> options) { _options = options; } public void Process() { var maxItems = _options.CurrentValue.MaxItems; } }

Unlike IOptions<T>, IOptionsMonitor<T> can provide the updated value when the configuration changes. It also allows us to register a callback that runs when the monitored configuration changes.

_options.OnChange(settings => { Console.WriteLine( $"MaxItems changed to {settings.MaxItems}"); });

The easiest way to remember the difference is that IOptions<T> is for configuration that doesn't need to change, IOptionsSnapshot<T> gives us a snapshot for the current request, and IOptionsMonitor<T> gives us access to the latest value and lets us respond to changes.

InterfaceConfiguration changesTypical use
IOptions<T>Not picked up automaticallyFixed application settings
IOptionsSnapshot<T>Available on the next requestPer-request configuration
IOptionsMonitor<T>Available through the current valueRuntime changes and change notifications

The important part is not choosing the most advanced interface. We should choose the one that matches how our application expects its configuration to behave.

Conclusion

The Options Pattern gives us a simple way to move from string-based configuration access to strongly typed settings. Instead of spreading configuration keys throughout the application, we can group related settings into a class and inject that configuration where it is needed.

IOptions<T> is usually the right choice when configuration remains fixed, while IOptionsSnapshot<T> and IOptionsMonitor<T> are useful when the application needs to work with configuration changes at runtime.

The same Options Pattern can also work with configuration sources other than appsettings.json. This becomes particularly useful when configuration contains sensitive values that should not be stored in the application itself. In those cases, services such as Azure Key Vault can provide the configuration while the application continues to consume it through the same strongly typed approach.

Source Code

The complete source code and working demo for this article are available on GitHub: