Date:

Share:

Understanding IOptions, IOptionsMonitor, and IOptionsSnapshot in .NET 7 | Code4IT

Related Articles

Table of Contents

When we deal with configurations in a .NET application, we can choose different strategies. For example, you can simply inject IConfiguration instance in your constructor, and retrieve each value by name.

Or you can make the best of it strongly typed configurations – You won’t have to worry about manually imposing these values ​​as everything is already done for you by .NET.

In this article, we are going to learn about IOptions, IOptionsSnapshotand IOptionsMonitor. They look similar, but there are some key differences that you need to understand to choose the right one.

For the sake of this article, I created a dummy .NET API that exposes only one endpoint.

in my appssettings.json file, I added a node:

{
    "MyConfig": {
        "Name": "Davide"
    }
}

which will be mapped to the POCO class:

public class GeneralConfig
{
    public string Name { get; set; }
}

To add it to the API project, we can add this line to Program.cs file:

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

As you can see, it takes the content with the name “MyConfig” and maps it to an object of type GeneralConfig.

To test such types, I created a set of dummy API controllers. Each controller exposes a single method, Get()which reads the value from the configuration and returns it in the object.

We are going to inject IOptions, IOptionsSnapshotand IOptionsMonitor into the constructor of the associated controllers so that we can try the different approaches.

IOptions: Simple, Singleton, does not support reloading settings

IOptions<T> is the simplest way to inject such configurations. We can inject it into our constructors and access the actual value using the Value attribute:

private readonly GeneralConfig _config;

public TestingController(IOptions<GeneralConfig> config)
{
    _config = config.Value;
}

Now we have direct access to GeneralConfig object, where the values ​​are populated as we defined in appssettings.json file.

There are a few things to consider when using IOptions<T>:

  • This service is injected as A An example of a singleton: The entire application uses the same instance, and is valid throughout the life of the application.
  • All configurations are read at runtime. Even if you update the App settings file while the application is running, you will not see the values ​​updated.

Some people prefer to store the IOptions<T> instance as a private field, and access the value when necessary using the Value property, like this:

private readonly IOptions<GeneralConfig> _config;
private int updates = 0;

public TestingController(IOptions<GeneralConfig> config)
{
    _config = config;
}

[HttpGet]
public ActionResult<Result> Get()
{
    var name = _config.Value.Name;
    return new Result
    {
        MyName = name,
        Updates = updates
    };
}

It works, but it’s useless: since IOptions<T> is a Singleton, we don’t need to access Value property at a time. It won’t change over time, and accessing it every time is a futile action. We can use the previous approach: easier to write and (just a bit) more performant.

One more thing. when writing unit testsWe can inject IOptions<T> In the system tested using a static method, Options.Create<T>and deliver him a show of the required type.

[SetUp]
public void Setup()
{
    var config = new GeneralConfig { Name = "Test" };

    var options = Options.Create(config);
    
    _sut = new TestingController(options);
}

Demo: The configuration does not change at runtime

Below you can find a GIF that shows that the configurations do not change when the app is running.

As you can see, I performed the following steps:

  1. Run the application
  2. call to /TestingIOptions Endpoint: It returns the name Davide
  3. I am now updating the content of appssettings.json File, name definition for Davide Bellone.
  4. When I call the same endpoint again, I don’t see the updated value.

IOptionsSnapshot: In scope, less performance, supports reloading configuration

similar to IOptions<T> We have IOptionsSnapshot<T>. They work in a similar way, but there is a huge difference: IOptionsSnapshot<T> Recalculated on each request.

public TestingController(IOptionsSnapshot<GeneralConfig> config)
{
    _config = config.Value;
}

With IOptionsSnapshot<T> You always have the most up-to-date values: This service is injected with a lifetime, meaning the values ​​are read from the configuration in every HTTP request. It also means that You can update the settings values ​​while the application is runningAnd you can see the updated results.

Because .NET rebuilds the configurations on each HTTP call, There is an easy performance above the head. So if not needed, always use IOptions<T>.

There is no way to check a IOptionsSnapshot<T> as we did with IOptions<T>So you need to use stubs or mocks (maybe with Moq or NSsubstitute 🔗).

Demo: The configuration changes while the application is running

Take a look at the GIF below: Here I’m running the app and calling /TestingIOtionsSnapshot endpoint.

With IOptionsSnapshot the configuration changes while the application is running

I performed the following steps:

  1. run the application
  2. call to /TestingIOtionsSnapshot endpoint. The return value is the same as in appssettings.json File: Davide Bellone.
  3. I then update the value in the configuration file
  4. When you call again /TestingIOtionsSnapshotI can see that the returned value reflects the new value in App settings file.

IOptionsMonitor: Complex, Singleton, supports configuration reload

Finally, the last of the trio: IOptionsMonitor<T>.

Through IOptionsMonitor<T> You can get the most updated value in the appsettings.json file.

We also have a callback event that fires whenever the configuration file is updated.

It is injected as a Singleton service, so the same instance is shared throughout the life of the application.

There are two main differences with IOptions<T>:

  1. The name of the property that stores the configuration value is CurrentValue instead of Value;
  2. There is a callback that is called every time you update the settings file: OnChange(Action<TOptions, string?> listener). You can use it to perform actions that should be run whenever the configuration changes.

Note: OnChange Returns an object that implements IDisposable that you need to get rid of. otherwise, like Chris Albert took notice (PS: follow him on Twitter!) The instance of the using class IOptionsMonitor<T> will never be fired.

Again, there is no way to check a IOptionsMonitor<T> as we did with IOptions<T>. So you should rely on stubs and mocks (again, maybe with Moq or NSsubstitute 🔗).

Demo: The configuration is changed, and the callback is called

In the GIF below I demonstrate the use of IOptionsMonitor.

I created an API controller that listens for configuration changes, updates a static counter and returns the final result from the API:

public class TestingIOptionsMonitorController : ControllerBase
{
    private static int updates = 0;
    private readonly GeneralConfig _config;

    public TestingIOptionsMonitorController(IOptionsMonitor<GeneralConfig> config)
    {
        _config = config.CurrentValue;
        config.OnChange((_, _) => updates++);
    }

    [HttpGet]
    public ActionResult<Result> Get() => new Result
    {
        MyName = _config.Name,
        Updates = updates
    };
}

By running it and changing the configuration content while the app is running, you can see the full usage of IOptionsMonitor<T>:

With IOptionsMonitor the configuration is changed, the callback is called

As you can see, I performed the following steps:

  1. run the application
  2. call to /TestIOptionsMonitor endpoint. The MyName field is called from config, and updates is 0;
  3. I then update and save the configuration file. In the background, the OnChange A callback is fired, and the update value is updated;

Strangely, the callback is called more times than expected. I only updated the file twice, but the counter is set to 6. This is strange behavior. If you know why this is happening, send a message below 📩

IOptions vs IOptionsSnapshot vs IOptionsMonitor in .NET

We have seen a brief introduction to IOptions, IOptionsSnapshotand IOptionsMonitor.

There are some differences, of course. Here is a table with a summary of what we learned from this article.

Type DI Lifetime The best way to inject in unit tests Enables live reloading Has a callback function
IOptions<T> A single card Options.Create<T>
IOptionsSnapshot<T> in scope Badel / Mok 🟢
IOptionsMonitor<T> A single card Badel / Mok 🟢 🟢

Actually there is more: for example, with IOptionsSnapshot and IOptionsMonitor you can use name optionsSo you can inject more instances of the same type that refer to different nodes in the JSON file.

But that will be the topic for a future article, so stay tuned 😎

Additional readings

There is much more on how to inject configurations.

For sure, one of the best resources is the official documentation:

🔗 Options Pattern in ASP.NET Core | Microsoft docs

I insisted on explaining it IOptions and IOptionsMonitor they A single cardOn time IOptionsSnapshot he in scope. If you don’t know what they mean, here’s a short but thorough explanation:

🔗 .NET Dependency Injection Lifetime | Code4IT

In particular, I want you to focus on a bonus tip, where I explain the problems that exist passing or in scope Services injected into a A single card service:

🔗Bonus tip: Transient dependencies inside a singleton | Code4IT

This article first appeared on Code4IT 🐧

In this article, I stored my configurations in the appsettings.json file. There are other ways to set configuration values ​​- for example, environment variables and lanceSettings.json.

🔗 3 (and more) ways to set configuration values ​​in .NET | Code4IT

finishing

In this article we learned how to use IOptions<T>, IOptionsSnapshot<T>and IOptionsMonitor<T> NET 7 application.

There are many other ways to handle configurations: for example, you can simply inject the entire object as a singleton, or use IConfiguration to get individual values.

When would you choose an approach over another? Leave a comment below! 📩

I hope you enjoyed this article! Let’s keep in touch Twitter or LinkedIn! 🤜🤛

Happy coding!

🐧

.

Source

Popular Articles