Net6 configuration & Options source code analysis part2 options model usage and source code analysis

Time:2022-5-11

Net6 configuration & Options source code analysis part2 options

Part II main recordsOptions model
The optionsconfigurationservicecollectionextensions class provides a pair ofOptions modelAndConfiguration systemExtension of the configure method of

1. Use options directly

Use options directly

In the startup configservice, you often see a ramda registered as a configuration item, for example:.Configure(it ->it.age = 18), we call this ramdaConfigure ActionIn fact, it uses a wrapper class to wrap your configure action delegate and register the instance of this class in the service container. It realizes the mapping between ioptions and ramda, which is determined byOptionsServiceCollectionExtensionsOptionsFacotryAnd so on. You can also use it directly like this

var profile = new Servicecollection ().Addoptions().Configure(it ->it.age = 18).BuildServiceProvider().GetRequiredService>().Value;

Configuration service registration source code analysis/Configure ActionRegistration of packaging

Optionsservicecollectionextensions is an extension class of configuration services. There are three types of extension methods: configure, postconfigure and addoptions. The first two corresponding services are iconfigureoptions and ipostconfigureoptions. The difference is only to realize the execution time of configuration action. Ipostconfigureoptions will be executed after the meeting, while addoptions is essentially the first two registered, It seems that the configuration action registered by addoptions has parameters and can access other services in di.
Note: even if you use three registration methods to register the same toptions one or more times, they actually operate the same toptions to you. This is reflected in optionsfactory Create is also the effect we want.

The following code is the service registration logic of configure and postconfigure.

The configure action registered by configure, postconfigure and extension methods will be wrapped by the wrapper class corresponding to iconfigureoptions and ipostconfigureoptions interfaces. The properties are all actions, and the execution of configure action is in the configure method. IConfigureOptionsWith ipostconfigureoptionsA large number of generic classes are used to access other services in di using the configure action registered with addoptions. It will be recorded separately below.

It is worth noting that if you use this method to inject a configure action with null name, the packaging class logic is embodied in configurenamedoptions Configure/PostConfigureOptions. Configure method., The official statement: “configure all options instances, both named and default” post translation extension method applies the configuration to all options, including named instances and default instances.

Configure -> ConfigureNamedOptions
PostConfigure -> PostConfigureOptions

public static class OptionsServiceCollectionExtensions
{
    ...
   public static IServiceCollection Configure(this IServiceCollection services!!, string? name, Action configureOptions!!)
            where TOptions : class
    {
        services.AddOptions();
        services.AddSingleton>(new ConfigureNamedOptions(name, configureOptions));
        return services;
    }

    public static IServiceCollection PostConfigure(this IServiceCollection services!!, string? name, Action configureOptions!!)
    where TOptions : class
    {
        services.AddOptions();
        services.AddSingleton>(new PostConfigureOptions(name, configureOptions));
        return services;
    }
    ...
}
public class ConfigureNamedOptions : IConfigureNamedOptions where TOptions : class
{
    public ConfigureNamedOptions(string? name, Action? action)
    {
        Name = name;
        Action = action;
    }
    public virtual void Configure(string? name, TOptions options!!)
    {
        // Null name is used to configure all named options.//  Name filtering and configure logic are embodied here
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}
public class PostConfigureOptions : IPostConfigureOptions where TOptions : class
{
    public PostConfigureOptions(string? name, Action? action)
    {
        Name = name;
        Action = action;
    }

    public virtual void PostConfigure(string? name, TOptions options!!)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}
The following code is the service registration logic of addoptions.

Addoptions: this method will help you build an optionsbuilder. Instead of injecting into the service container, it uses the configure method of its builder class to inject into the service container. The following overloaded configure methods will help you create different numbers of generic classes based on iconfigurenamedoptions / ipostconfigureoptions Its purpose is to solve the problem of“Configure Action”Use other services in.
The design idea is very good and can be used for reference

The overall idea is to use addoptions firstThe extension method creates an optionsbuilderObject, and then call it to overload the method configureTo create configurenamedoptions with multiple genericsObject. When executing the action delegate, the configure of configurenamedoptions will use the serviceprovider to obtain the tservice generic service. Pass in the action delegate as a parameter. In this way, the delegation will get the corresponding service when it is actually executed.

public static class OptionsServiceCollectionExtensions
{
    ...
    public static OptionsBuilder AddOptions(this IServiceCollection services!!, string? name)
        where TOptions : class
    {
        services.AddOptions();
        return new OptionsBuilder(services, name);
    }
    ...
}
public class OptionsBuilder where TOptions : class
{
    ...
    public virtual OptionsBuilder Configure(Action configureOptions!!) where TDep : class
    {
        Services.AddTransient>(sp =>
            new ConfigureNamedOptions(Name, sp.GetRequiredService(), configureOptions));
        return this;
    }
    ...
}

public class ConfigureNamedOptions : IConfigureNamedOptions {
    ...
    public ConfigureNamedOptions(string? name, TDep dependency, Action? action)
    {
        Name = name;
        Action = action;
        Dependency = dependency;
    }

    public virtual void Configure(string? name, TOptions options!!)
    {
        // Null name is used to configure all named options.
        if (Name == null || name == Name)
        {
            Action?.Invoke(options, Dependency);
        }
    }
    ...
}

summary

Extensions injected into the service (optionsservicecollectionextensions) Service class Service implementation class use life cycle remarks
Configure IConfigureOptions ConfigureNamedOptions Used by optonsfactory Singleton Configureall, execute configure action before ipostconfigureoptions
PostConfigure IPostConfigureOptions PostConfigureOptions Used by optonsfactory Singleton Configureall executes the configure action after iconfigureoptions
AddOptions IConfigureOptions/IPostConfigureOptions ConfigureNamedOptions/PostConfigureOptions Used by optonsfactory Singleton Assist in injecting a configure action that can access other services

Configure the usage of otpns service

First, the services injected in the inherent ideas will be used directly. The injected services here are iconfigureoptions / ipostconfigureoptions services. We call them the wrapper class of configure action, and these services are obtained through ioptions / ioptions snapshot / ioptions monitor.We call these three services the optionsmanger class

Registered basic services (optionsservicecollectionextensions. Addoptions)

Here are the differences between the three optionsmangers of ioptions / iopptionsmanshot / iopptionsmonitor.
Service class | service implementation class | use | life cycle | remarks|
—|:–😐:–😐:–😐:–😐–:
IOptions|UnnamedOptionsManager|Directly read the configuration data after di|singleton| starts the application, and no new changes can be obtained after the configuration is updated. Implementation logic: directly call optionsfactory Create (options. Defaultname) gets the wrapper classes of all configure actions and executes the wrapper class configuration method|
IOptionsSnapshot|OptionsManager|Use the same as above to distinguish between iosscope |, when accessing and caching options for request lifetime, the options are calculated once for each request. When using a configuration provider that supports reading updated configuration values, changes to the configuration are read after the application starts. Implementation logic: cache after creating with factory. Very simple|
IOptionsMonitor|OptionsMonitor|The difference between the above and singleton is that when the configuration changes, the configuration provided by him will be updated in real time|

Ioptions using demo

var source = new Dictionary{
    {"TestOptions:Key1" ,"TestOptions key1"},
};
var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure(config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
IOptions options = serviceProvider.GetService>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOptions
{
    public string Key1 { get; set; }
}

Ioptions using source code analysis

Take the first example: the get accessor of the value attribute of unnamed optionsmanager directly calls optionsfactory. craete create is even simpler. The constructor takes all iconfigureoptions/IPostConfigureOptionsGot it. Directly loop the two collections and call the configure method to OK. The injected action is executed inside the caonfigure to operate on the toptions instance.

internal sealed class UnnamedOptionsManager : IOptionswhere TOptions : class
    {
        private volatile TOptions? _value;
        public UnnamedOptionsManager(IOptionsFactory factory) => _factory = factory;
        public TOptions Value
        {
            get
            {
                if (_value is TOptions value)
                {
                    return value;
                }
                return _value ??= _factory.Create(Options.DefaultName);
            }
        }
    }
public class OptionsFactory :
    IOptionsFactory
    where TOptions : class
{
    ...

    public TOptions Create(string name)
    {
        //Create topics instance object
        TOptions options = CreateInstance(name);
        //Call the registered configure action in turn
        //First execute the configure action wrapped in iconfigureoptions
        foreach (IConfigureOptions setup in _setups)
        {
            if (setup is IConfigureNamedOptions namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        //When executing the configure action wrapped in ipostconfigureoptions
        foreach (IPostConfigureOptions post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }
        //Execute validation logic
        if (_validations.Length > 0)
        {
            var failures = new List();
            foreach (IValidateOptions validate in _validations)
            {
                ValidateOptionsResult result = validate.Validate(name, options);
                if (result is not null && result.Failed)
                {
                    failures.AddRange(result.Failures);
                }
            }
            if (failures.Count > 0)
            {
                throw new OptionsValidationException(name, typeof(TOptions), failures);
            }
        }

        return options;
    }
    ...
}

Ioptions snapshot using demo

Since the implementation logic is basically the same as ioptons, no more records will be made here.

var source = new Dictionary{
    {"TestOptions:Key1" ,"TestOptions key1"},
};
var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure("TestOptions", config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
IOptionsSnapshot optionsAccessor = serviceProvider.GetRequiredService>();
Console.WriteLine(optionsAccessor.Get("TestOptions").Key1);
Console.ReadLine();
public class TestOptions
{
    public string Key1 { get; set; }
}

Configure synchronous ioptionsmonitor for sourceUse demo of

The source code is analyzed separately in part3.

var configuration = new ConfigurationBuilder().AddJsonFile(path: "profile.json",
                                                           optional: false,
                                                           reloadOnChange: true).Build();
new ServiceCollection().AddOptions().Configure(configuration).BuildServiceProvider().GetRequiredService>().OnChange(profile => Console.WriteLine($"changed: {profile.Age}"));
Console.Read();

public class Profile
{
    public int Age { get; set; }
}

“Options model”And“Configure system”combination.

2. Bind the configuration as an options object

The following two demos demonstrate“Options model”And“Configure system”Combined use of.
Demo1

var configuration = new ConfigurationBuilder ().AddJsonFile ("profile.json").Build ();
var profile = new ServiceCollection().AddOptions().Configure(configuration).BuildServiceProvider().GetRequiredService>().Value;

Demo2

var source = new Dictionary{
    {"TestOptions:Key1" ,"TestOptions key1"},
    {"TestOptions:Key2" ,"TestOptions key2"},
    {"UserInfo:key1" ,"UserInfo"},
};

var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure(config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOpetion{
    public string Key1{ get; set; }
    public string Key2 { get; set; }
}

The above operation steps define the extension to configure for the optionsconfigurationservicecollectionextensions class. There are three parameters: String name, config (iconfiguration), and the delegation of configurebinder. The first parameter is the name of toptions, the second represents the iconfiguration of the configuration system, and the third configurebinder is some configurations of the configuration system when mapping topics

The principle is very simple. With the support of servicecollection, we can inject a type iconfigureoptions into itClass that is actually new namedconfigurefromconfigurationoptions. Then the namedconfigurefromconfigurationoptions class constructorThe bindfromoptions method is passed to the parent class configurenamedoptions as a configure actionIn other words, this class helps us provide a configure action that calls configurebinder. (config. Bind (options, configurebinder): Bing is an extension method and configurationbinder is a help class.). In this way, the only function of namedconfigurefromconfigurationoptions is to help us organize a configure action so that you don’t write it yourself.
aboutconfigureBinderThe basic logic is basically to reflect the information according to the type object of toptions, and then the second parameter config (configure the interface for data provided by the system) takes the data and binds the corresponding data to the toptions object.

public static IServiceCollection Configure(this IServiceCollection services!!, string? name, IConfiguration config!!, Action? configureBinder)
    where TOptions : class
{
    services.AddOptions();
    //It is used to support the callback function registered when calling back options when the configuration system is updated after * * configuring the system * * is combined with * * options model * *. We'll talk about it later
    services.AddSingleton>(new ConfigurationChangeTokenSource(name, config));
    //Register namedconfigurefromconfigurationoptions
    return services.AddSingleton>(new NamedConfigureFromConfigurationOptions(name, config, configureBinder));
}

    /// Configures an option instance by using  against an .
    public class NamedConfigureFromConfigurationOptions : ConfigureNamedOptions where TOptions : class
    {
        public NamedConfigureFromConfigurationOptions(string? name, IConfiguration config!!, Action? configureBinder)
            : base(name, options => BindFromOptions(options, config, configureBinder)){
        }
        
        private static void BindFromOptions(TOptions options, IConfiguration config, Action? configureBinder) => config.Bind(options, configureBinder);
    }
    
    public class BinderOptions
    {
        //True will also assign a value to the private property of toptons
        public bool BindNonPublicProperties { get; set; }
 
        public bool ErrorOnUnknownConfiguration { get; set; }
    }

Verify the validity of options

Microsoft options extension method registration Extensions. Options injects the authentication service into the service container. The principle is optionsfactory Create gets all the injected services. Pass totons as a parameter into the verification method of the instance.

services.AddOptions().Configure(options =>options. DatePattern = datePattern;options.TimePattern = timePattern;).Validate(options => Validate (options.DatePattern) && Validate(options. TimePattern), "Invalid Date or Time pattern.");

other

OptionsServiceCollectionExtensions Options modelDependent services

public static IServiceCollection AddOptions(this IServiceCollection services)
{
    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
    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<>)));
}