IOptions, IOptions monitor, and IOptions snapshot

background

ASP.NET Core introduces the Options mode, which uses classes to represent related setting groups. To put it simply, a strongly typed class is used to express configuration items, which brings many benefits.
Beginners will find that this framework has three main consumer oriented interfaces: ioptions < toptions >, ioptions monitor < toptions > and ioptions snapshot < toptions >.
These three interfaces look similar at first, so it is easy to cause confusion. Which interface should be used in what scenarios?




Example

Let's start with a small piece of code (the TestOptions class has only one string attribute Name, and the code is omitted):

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         var builder = new ConfigurationBuilder();
 6         builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); //Note the last parameter value. true indicates that the configuration file will be reloaded when it changes.
 7         var configuration = builder.Build();
 8         var services = new ServiceCollection();
 9         services.AddOptions();
10         services.Configure<TestOptions>(configuration); //Here, test options is bound through the configuration file
11         var provider = services.BuildServiceProvider();
12         Console.WriteLine("Before amendment:");
13         Print(provider);
14 
15         Change(provider); //Modify with code Options Value.
16         Console.WriteLine("After code modification:");
17         Print(provider);
18         
19         Console.WriteLine("Please modify the configuration file.");
20         Console.ReadLine(); //Waiting for manual modification appsettings.json Profile.
21         Console.WriteLine("modify appsettings.json After the document:");
22         Print(provider);
23     }
24 
25     static void Print(IServiceProvider provider)
26     {
27         using(var scope = provider.CreateScope())
28         {
29             var sp = scope.ServiceProvider;
30             var options1 = sp.GetRequiredService<IOptions<TestOptions>>();
31             var options2 = sp.GetRequiredService<IOptionsMonitor<TestOptions>>();
32             var options3 = sp.GetRequiredService<IOptionsSnapshot<TestOptions>>();
33             Console.WriteLine("IOptions value: {0}", options1.Value.Name);
34             Console.WriteLine("IOptionsMonitor value: {0}", options2.CurrentValue.Name);
35             Console.WriteLine("IOptionsSnapshot value: {0}", options3.Value.Name);
36             Console.WriteLine();
37         }
38     }
39 
40     static void Change(IServiceProvider provider)
41     {
42         using(var scope = provider.CreateScope())
43         {
44             var sp = scope.ServiceProvider;
45             sp.GetRequiredService<IOptions<TestOptions>>().Value.Name = "IOptions Test 1";
46             sp.GetRequiredService<IOptionsMonitor<TestOptions>>().CurrentValue.Name = "IOptionsMonitor Test 1";
47             sp.GetRequiredService<IOptionsSnapshot<TestOptions>>().Value.Name = "IOptionsSnapshot Test 1";
48         }
49     }
50 }

appsettings.json file:

{
    "Name": "Test 0"
}

In the above code, first read the configuration from the appsettings.json file, then register the TestOptions that depend on the configuration file with the container, and then print the values of ioptions < >, ioptions monitor < > and ioptions snapshot < > respectively.

Then modify the value of TestOptions through code and print.
Then modify the value of TestOptions by modifying the appsettings.json file and print it.

Note that we only register TestOptions once, but we can get the values of TestOptions through the ioptions < > interface, ioptionsmonitor < > interface and ioptionsnapshot < > interface respectively.

If we change the value of Name in appsettings.json file to Test 2, the output of the above code is as follows:




Analysis

We can see that after modifying the values of ioptions < > and ioptionsmonitor < > for the first time through code, they are updated again, but ioptionsnapshot < > is not. Why?
Let's start with the source code of the Options framework and understand why.
When we need to use the Options mode, we all call the extension method AddOptions(this IServiceCollection services) defined on the OptionsServiceCollectionExtensions class.

var services = new ServiceCollection();
services.AddOptions();

We observe the implementation of AddOptions method:

public static IServiceCollection AddOptions(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }

    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
    services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
    services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
    return services;
}

From the above code, we can see that ioptions < > and ioptions monitor < > are registered as single instance services, while ioptions snapshot < > is registered as range services.
Since both ioptions and ioptionsmonitor are registered as single instance services, the same instance is obtained each time, so the changed value is reserved.
Ioptionsnapshot < > is registered as a scope service, so each time a new scope is created, a new value is obtained. External changes are only valid for the current time and will not be retained until the next time (cannot cross scope, cannot cross request for ASP.NET Core).

Let's continue to see the second modification. After the second modification of the configuration file, the values of ioptionsmonitor < > and ioptionsnapshot < > are updated, while the values of ioptions < > are not.
Ioptions < > is easy to understand. It is registered as a single instance service. When it is accessed for the first time, the instance is generated and the value in the configuration file is loaded. After that, the configuration file will not be read again, so its value will not be updated.
Ioptionssnapshot < > is registered as a scope service. Every time a new scope is regenerated, it will get the value from the configuration file, so its value will be updated.
However, the ioptionsmonitor < > is registered as a single instance. Why is it updated?
Let's go back to the source code of AddOptions and notice that the implementation of ioptionsmonitor < > is optionsmanager < >.
When we open the source code of options manager, everything is clear.
Its constructor is as follows:





public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
    _factory = factory;
    _sources = sources;
    _cache = cache;

    foreach (var source in _sources)
    {
        var registration = ChangeToken.OnChange(
                () => source.GetChangeToken(),
                (name) => InvokeChanged(name),
                source.Name);

        _registrations.Add(registration);
    }
}

It turns out that the update capability of OptionsMonitor comes from ioptions change token source < Options >. But who is the instance of this interface?
Let's go back to line 10 of the original code:

services.Configure<TestOptions>(configuration);

This is an extension method defined in Microsoft.Extensions.Options.ConfigurationExtensions.dll. Finally, an overloaded method is actually called. The code is as follows:

 1 public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
 2     where TOptions : class
 3 {
 4     if (services == null)
 5     {
 6         throw new ArgumentNullException(nameof(services));
 7     }
 8 
 9     if (config == null)
10     {
11         throw new ArgumentNullException(nameof(config));
12     }
13 
14     services.AddOptions();
15     services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
16     return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
17 }

The secret is on line 15 above, configurationchangetoken source, which refers to the object config representing the configuration file, so if the configuration file is updated, the IOptionsMonitor will be updated.


conclusion

Ioptions < > is a single example, so once generated, its value will not be updated unless it is changed by code.
Ioptionsmonitor < > is also a single example, but it can be updated with the configuration file through ioptionschaetokensource < > and can also change the value by code.
Ioptionsnapshot < > is a range, so its value will be updated in the next access of profile update, but it can't change the value by code across the range, and it can only be valid in the current range (request).

The official documents are as follows:
Ioptionsmonitor < TOptions > is used to retrieve options and manage options notifications for TOptions instances. It supports the following scenarios:

  • Instance update notification.
  • Name the instance.
  • Reload the configuration.
  • Selectively invalidate the instance.

Ioptions snapshot < Options > is useful in scenarios where options need to be recalculated for each request.
Ioptions < Options > can be used to support options mode, but it does not support the scenarios supported by the previous two. If you do not need to support the above scenarios, you can continue to use ioptions < Options >.

So you should choose which of the three to use according to your actual use scenario.
Generally speaking, if you rely on configuration files, consider ioptionsmonitor < > first, if not, then ioptionsnapshot < > and finally ioptions < >.
It should be noted that in ASP.NET Core applications, IOptionsMonitor may cause inconsistent values of options in the same request - when you are modifying the configuration file - which may cause some strange bug s.
If this is important to you, please use ioptions snapshot, which can guarantee the consistency in the same request, but it may cause slight performance loss.
If you construct Options when the app starts (for example, in the Startup class):


services.Configure<TestOptions>(opt => opt.Name = "Test 0");

Ioptions < > is the simplest, maybe a good choice. There are other overloads in the Configure extension method to meet your needs.

Keywords: JSON snapshot Attribute

Added by eabigelow on Fri, 17 Apr 2020 11:08:00 +0300