Demystifying IOptions in .NET Core — A Gateway to Efficient Configuration Management

Configuration management, IConfiguration
.NET Core’s robust application settings and configuration management are well-known and widely appreciated among
developers. At the core of this system is the versatile IConfiguration
interface, merging data from various sources
such as appsettings.json
, user secrets, environment variables, and more. But have you ever wondered how to fetch a
value from this broad array of configuration data efficiently?
Sure, you might use something as straightforward as var foo = _configuration["DoesNotExist"];
. Yet, there’s a major
drawback: it’s quite messy. You’re dealing with a magic string - a code smell that’s best avoided for clean,
maintainable code. Moreover, this approach can return null, which again, isn’t recommended.
TryGet, method extension way
Instead, many developers favor using Result<T>
, implementing a TryGet
approach. But is that really the best method?
Let’s take a closer look:
public static bool TryGetSection<T>(this IConfiguration configuration, string key, out T? value)
{
var section = configuration.GetSection(key);
if (section.Exists())
{
value = section.Get<T>();
return true;
}
value = default!;
return false;
}
On the surface, this approach seems more secure: if the key exists, we retrieve the value and return true; otherwise, we return false with a default value for type T. However, we still face a potential pitfall: in C#, reference types can be null, so if you try to access a nonexistent configuration section and use the result without verifying it’s not null, you risk running into a NullReferenceException.
What if we could improve this process, achieving stronger typing and avoiding NullReferenceExceptions?
IOptions and IOptionsSnapshot
That’s where IOptions
and IOptionsSnapshot
in .NET Core come into play, offering a fail-fast approach to
configuration management that provides type safety and eliminates the headache of null checks.
eg: When working with data in .NET applications, you often store sensitive settings like connection strings in your
configuration. The traditional way to retrieve these settings is through the IConfiguration
interface like so:
var connectionString = _configuration.GetSection("ConnectionStrings:DefaultConnection").Value;
This approach, while quick and easy, has its downsides. If we misspell the section or key name, we might receive a null
value, or even worse, a runtime exception. Surely, there must be a safer, cleaner way to handle this. Enter IOptions
.
With IOptions
, you can bundle your configuration into dto, providing a type-safe way to access your settings. Suppose
we want to access a database connection string. We can create a DTO as follows:
public class PersistenceOptions
{
public string ConnectionString { get; set; }
}
We then register it in our dependency injection container:
builder.Services.Configure<PersistenceOptions>(builder.Configuration.GetSection("ConnectionStrings"));
Now, when you need to access the connection string in your controller, you can do so like this:
public class WeatherForecastController
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly string _connectionString;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IOptions<PersistenceOptions> options)
{
_logger = logger;
_connectionString = options.Value.ConnectionString;
}
}
With IOptions
, there’s no risk of null values or exceptions from typos. It’s a clean, type-safe approach to managing
configuration.
IOptionsSnapshot, what is it ??
Now, let’s imagine a scenario where your application’s configuration might change while it’s running. This is
where IOptionsSnapshot
comes in.
IOptionsSnapshot
works similarly to IOptions
, but it reloads the configuration options each time they’re requested.
This ensures you always have the most up-to-date configuration values.
Let’s use the same PersistenceOptions
class from before, but this time with IOptionsSnapshot
:
public class WeatherForecastController
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly string _connectionString;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IOptionsSnapshot<PersistenceOptions> optionsSnapshot)
{
_logger = logger;
_connectionString = optionsSnapshot.Value.ConnectionString;
}
}
In this example, IOptionsSnapshot<PersistenceOptions>
re-loads the configuration values each time they’re requested,
ensuring that you always have the most up-to-date configuration values.
By using IOptions
and IOptionsSnapshot
, you can effectively manage your application’s configuration in a robust,
type-safe manner.
Stop ! Did you notice that I am still using magic string ?
However, you might have noticed that we’re still relying on a so-called “magic string” to identify our configuration section. This can lead to the same pitfalls we’ve been trying to avoid: misspellings, hard to refactor, and a lack of transparency about the string’s importance. But fear not, we can handle this concern neatly.
We can be using the Bind
method which provides an even more robust way to load configurations, further reducing the
chances of runtime errors due to missing or misnamed configuration sections or keys. Let’s see how it can be done.
First, let’s define a constant class to keep our configuration section names. This will help us avoid magic strings:
public static class ConfigurationKeys
{
public const string Persistence = "Persistence";
}
Next, define a PersistenceOptions
class that matches the structure of your configuration:
public class PersistenceOptions
{
public string ConnectionString { get; set; }
}
In the dependency injection setup, instead of calling Configure
, use the Bind
method:
var persistenceOptions = new PersistenceOptions();
builder.Configuration.GetSection(ConfigurationKeys.Persistence).Bind(persistenceOptions);
builder.Services.AddSingleton(persistenceOptions);
This approach has the added benefit of failing fast. If your configuration doesn’t match your PersistenceOptions
class, you’ll know immediately when the application starts, rather than at runtime.
Now, you can retrieve your settings in the same manner, this time injecting the PersistenceOptions
directly:
public class WeatherForecastController
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly string _connectionString;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
PersistenceOptions options)
{
_logger = logger;
_connectionString = options.ConnectionString;
}
}
This way, you eliminate the risk of typo-related exceptions and null values, while keeping your code clean and safe. Incredible right ??
But what if we want to detect whenever a modification has been made in our configuration file ?
IOptionsMonitor, the INotifyPropertyChanged’s little brother ?
In a sense, IOptionsMonitor
in ASP.NET Core can be thought of as a cousin to INotifyPropertyChanged
from the realm
of WPF, or other .NET frameworks that support data binding.
The INotifyPropertyChanged
interface is often used in MVVM applications to signal that a property value has changed,
and thus, the UI may need to update to reflect this change.
On the other hand, IOptionsMonitor
is used to track changes
in application’s configuration data. It provides a mechanism for your
code to react whenever modifications are made to the underlying
configuration data.
Let’s take an example where we want to watch for changes in our PersistenceOptions
.
Firstly, you inject IOptionsMonitor<PersistenceOptions>
into your controller:
public class WeatherForecastController
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IOptionsMonitor<PersistenceOptions> _optionsMonitor;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IOptionsMonitor<PersistenceOptions> optionsMonitor)
{
_logger = logger;
_optionsMonitor = optionsMonitor;
}
public string GetConnectionString()
{
return _optionsMonitor.CurrentValue.ConnectionString;
}
}
IOptionsMonitor
has a CurrentValue
property (like signal or event in javascript frameworks) that retrieves the most
recent value of the options. If the underlying PersistenceOptions
in the configuration changes, CurrentValue
will
reflect this change immediately.
But what if you want to perform some action when the options change, rather than just retrieving the latest value?
That’s where the OnChange
event comes into play. You can register a callback that will be invoked whenever the options
change:
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IOptionsMonitor<PersistenceOptions> optionsMonitor)
{
_logger = logger;
_optionsMonitor = optionsMonitor;
_optionsMonitor.OnChange(newOptions =>
{
_logger.LogInformation($"The connection string has changed to {newOptions.ConnectionString}");
});
}
With this setup, whenever the connection string in the configuration changes, a log message will be written with the new connection string.
IOptionsMonitor
is a powerful service in .NET that gives you fine-grained control over your configuration options.
Whether you need to react to configuration changes in real-time, or just want the latest options without the overhead of
creating a new snapshot for every request,
There is a thing for everything, that is explain why we all love dotnet.
So, there you have it. You’ve now mastered the art of using IOptions
in .NET. The next time you see a GetSection
in
your code, it might just make you smile. After all, now you have the powerful and elegant IOptions
at your fingertips!
Say goodbye to GetSection
, and hello to cleaner, clearer code. Happy coding!