Net6 Configuration & Options 原始碼分析 Part2 Options

一身大膘發表於2022-03-21

Net6 Configuration & Options 原始碼分析 Part2 Options

第二部分主要記錄Options 模型
OptionsConfigurationServiceCollectionExtensions類提供了對Options 模型配置系統的Configure方法的擴充套件

1. 直接使用Options

直接使用Options

在Starup ConfigService中經常會看到把一個拉姆達註冊成配置項例如:.Configure<Profile>(it ->it.age = 18),我們稱這個拉姆達為Configure Action,其實這是使用了一個包裝類,包裝你的Configure Action委託,並把這個類的例項註冊到Service容器中。它實現IOptions與拉姆達如何對映的,這一切由OptionsServiceCollectionExtensionsOptionsFacotry等實現的。你也可以直接像下面這樣使用

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

配置服務註冊原始碼分析/Configure Action包裝類的註冊

OptionsServiceCollectionExtensions 作為配置服務的擴充套件類下面有三種型別的擴充套件方法分別是Configure、PostConfigure、AddOptions,前兩所對應的服務為IConfigureOptions與IPostConfigureOptions 區別僅僅是為了實現配置Configure Action的執行時機,IPostConfigureOptions會後執行,而AddOptions本質上還是註冊前兩種,註冊成面上看起來AddOptions註冊的Configure Action具有了引數可以訪問其它DI內服務。
注意:就算你使用了三個註冊方式註冊一次或多次對同一個TOptions進行註冊,他們其實是操作的同一個TOptions給你。這體現在OptionsFactory.Create上,也是我們想要的效果。

以下程式碼為Configure、PostConfigure的服務註冊邏輯。

Configure、PostConfigure、擴充套件方法註冊的 Configure Action會由IConfigureOptions與IPostConfigureOptions 介面對應的包裝類進行包裝。屬性均是Action,Configure Action的執行是在Configure方法中。IConfigureOptions與IPostConfigureOptions的大量泛型類是為了實現使用AddOptions註冊的Configure Action訪問其它DI內服務。會在下面單獨記錄。

值得注意的是ConfigureAll如果你此方法去注入一個Name 為null 的 Configure Action包裝類邏輯體現在ConfigureNamedOptions.Configure/PostConfigureOptions.Configure方法上。,官方說法:“Configure ALL options instances, both named and default”翻譯後擴充套件方法將配置應用於所有選項,包括命名例項和預設例項。

Configure -> ConfigureNamedOptions
PostConfigure -> PostConfigureOptions

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

    public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services!!, string? name, Action<TOptions> configureOptions!!)
    where TOptions : class
    {
        services.AddOptions();
        services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));
        return services;
    }
    ...
}
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
    public ConfigureNamedOptions(string? name, Action<TOptions>? action)
    {
        Name = name;
        Action = action;
    }
    public virtual void Configure(string? name, TOptions options!!)
    {
        // Null name is used to configure all named options.// Name的過濾以及Configure 邏輯就是在這裡體現的
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}
public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
    public PostConfigureOptions(string? name, Action<TOptions>? action)
    {
        Name = name;
        Action = action;
    }

    public virtual void PostConfigure(string? name, TOptions options!!)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }
}

以下程式碼為AddOptions 的服務註冊邏輯。

AddOptions:此方法會幫你構建一個OptionsBuilder,並非向Service容器注入,而是利用其builder類的Configure方法向Service容器具體注入。其下面的大量過載 Configure方法會幫你建立基於IConfigureNamedOptions/IPostConfigureOptions不同數量的泛型類. 其目的就是為了解決在“ Configure Action”中使用其它服務。
設計思路很好可以參考

整體思路大概是,先用AddOptions擴充套件方法建立了一個OptionsBuilder物件,然後呼叫它過載方法Configure<TService...>去建立具有多個泛型的ConfigureNamedOptions物件。ConfigureNamedOptions的Configure在執行Action委託時會用serviceProvider獲取到TService泛型服務。作為引數傳入Action委託。這樣委託在真正被執行時就會拿到對應的服務。

public static class OptionsServiceCollectionExtensions
{
    ...
    public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services!!, string? name)
        where TOptions : class
    {
        services.AddOptions();
        return new OptionsBuilder<TOptions>(services, name);
    }
    ...
}

public class OptionsBuilder<TOptions> where TOptions : class
{
    ...
    public virtual OptionsBuilder<TOptions> Configure<TDep>(Action<TOptions, TDep> configureOptions!!) where TDep : class
    {
        Services.AddTransient<IConfigureOptions<TOptions>>(sp =>
            new ConfigureNamedOptions<TOptions, TDep>(Name, sp.GetRequiredService<TDep>(), configureOptions));
        return this;
    }
    ...
}

public class ConfigureNamedOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions> {
    ...
    public ConfigureNamedOptions(string? name, TDep dependency, Action<TOptions, TDep>? 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);
        }
    }
    ...
}

總結

注入到服務的擴充套件方法(OptionsServiceCollectionExtensions) 服務類 服務實現類 使用 生命週期 備註
Configure IConfigureOptions ConfigureNamedOptions 被OptonsFactory使用 Singleton ConfigureAll,在IPostConfigureOptions前執行Configure Action
PostConfigure IPostConfigureOptions PostConfigureOptions 被OptonsFactory使用 Singleton ConfigureAll 在IConfigureOptions後執行Configure Action
AddOptions IConfigureOptions/IPostConfigureOptions ConfigureNamedOptions/PostConfigureOptions 被OptonsFactory使用 Singleton 輔助注入一個可以訪問其它服務的Configure Action

配置Otpns服務的使用

首先在固有想法上注入的服務直接會拿來使用。而在這裡注入的均為IConfigureOptions/IPostConfigureOptions服務我們管他們叫Configure Action的包裝類,而要使用這些服務是通過IOptions/IOptionsSnapshot/IOptionsMonitor去獲得。我們稱這三個服務為OptionsManger類

註冊的基礎服務(OptionsServiceCollectionExtensions.AddOptions)

這裡整理出來IOptions/IOptionsSnapshot/IOptionsMonitor三種OptionsManger的區別。
服務類|服務實現類|使用|生命週期|備註|
---|:--?:--?:--?:--?--:
IOptions|UnnamedOptionsManager|直接在DI|Singleton|在應用啟動後讀取配置資料且配置更新後獲取不到新的變更。實現邏輯:直接呼叫OptionsFactory.Create(Options.DefaultName)拿到所有Configure Action的包裝類 執行包裝類配置方法|
IOptionsSnapshot|OptionsManager|同上|Scoped:|區別,通過使用 IOptionsSnapshot,針對請求生存期訪問和快取選項時,每個請求都會計算一次選項。 當使用支援讀取已更新的配置值的配置提供程式時,將在應用啟動後讀取對配置所做的更改。實現邏輯:用Factory建立完後得快取下。非常的簡單。|
IOptionsMonitor|OptionsMonitor|同上|Singleton|區別,當配置發生改變是由他提供的配置會實時更新。|

IOptions 使用Demo

var source = new Dictionary<string, string>{
    {"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();
IOptions<TestOptions> options = serviceProvider.GetService<IOptions<TestOptions>>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOptions
{
    public string Key1 { get; set; }
}

IOptions 使用原始碼分析

以第一個為例:UnnamedOptionsManager 的value屬性的get 訪問器其內部直接呼叫OptionsFactory.Craete Create就更簡單了。構造方法就把所有的 IConfigureOptions/IPostConfigureOptions拿到了。直接迴圈兩個集合,並呼叫Configure方法就Ok了 Caonfigure內部執行注入的Action 對TOptions例項一頓操作。

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

    public TOptions Create(string name)
    {
        // 建立Toptions 例項物件
        TOptions options = CreateInstance(name);
        // 依次呼叫註冊的 Configure Action
        // 先執行IConfigureOptions包裝的Configure Action.
        foreach (IConfigureOptions<TOptions> setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        // 在執行 IPostConfigureOptions包裝的Configure Action.
        foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }
        // 執行驗證邏輯
        if (_validations.Length > 0)
        {
            var failures = new List<string>();
            foreach (IValidateOptions<TOptions> 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;
    }
    ...
}

IOptionsSnapshot 使用Demo

因實現邏輯基本與IOptons相同這裡就不多做記錄。

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

配置源的同步 IOptionsMonitor 的使用 Demo

原始碼在part3 單獨分析。

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

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

"Options模型"“配置系統”結合。

2. 將配置繫結為Options物件

如下兩個demo分別演示了"Options模型"“配置系統”結合的結合使用。
Demo1

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

Demo2

var source = new Dictionary<string, string>{
    {"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<TestOpetion>(config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<TestOpetion>>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOpetion{
    public string Key1{ get; set; }
    public string Key2 { get; set; }
}

以上操作步驟為OptionsConfigurationServiceCollectionExtensions類定義了對 Configure的擴充套件,有三個引數string name、config(IConfiguration),configureBinder的委託,第一個引數是TOptions的name, 第二個表示配置系統的IConfiguration,第三個configureBinder 是配置系統在對映Toptions時候的一些配置

原理很簡單,因為有了ServiceCollection的支援,那麼就往裡面幫我們注入一個型別為IConfigureOptions 實際為new NamedConfigureFromConfigurationOptions的類。那麼NamedConfigureFromConfigurationOptions類建構函式裡面直接把BindFromOptions方法作為Configure Action傳給父類ConfigureNamedOptions 也就是說此類是幫我們提供了一個呼叫了configureBinder的Configure Action。(config.Bind(options, configureBinder)這個Bing是擴充套件方法,ConfigurationBinder是個幫助類)。這樣看來NamedConfigureFromConfigurationOptions唯一的作用就是幫幫我們組織了一個Configure Action免得你去自己寫了。
關於configureBinder基本邏輯基本是根據TOptions的Type 物件反射出資訊,然後第二個引數config(配置系統提供資料的介面)拿資料,在把對應的資料繫結在TOptions 物件上。

public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services!!, string? name, IConfiguration config!!, Action<BinderOptions>? configureBinder)
    where TOptions : class
{
    services.AddOptions();
    // 用於支援**“配置系統”**與 **"Options模型"**結合後當配置系統發生更新時回撥options時註冊的回撥函式。後面會說到
    services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
    // 註冊NamedConfigureFromConfigurationOptions
    return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}

    /// Configures an option instance by using <see cref="ConfigurationBinder.Bind(IConfiguration, object)"/> against an <see cref="IConfiguration"/>.
    public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions> where TOptions : class
    {
        public NamedConfigureFromConfigurationOptions(string? name, IConfiguration config!!, Action<BinderOptions>? configureBinder)
            : base(name, options => BindFromOptions(options, config, configureBinder)){
        }
        
        private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions>? configureBinder) => config.Bind(options, configureBinder);
    }
    
    public class BinderOptions
    {
        // true 會對TOptons的私有屬性也賦值
        public bool BindNonPublicProperties { get; set; }
 
        public bool ErrorOnUnknownConfiguration { get; set; }
    }

驗證Options的有效性

Options 擴充套件方法註冊Microsoft.Extensions.Options向Service容器注入認證服務,其原理是OptionsFactory.Create拿到所有注入的服務。將TOptons作為引數傳入例項的驗證方法。

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

其它

OptionsServiceCollectionExtensions Options 模型依賴的服務

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<>)));
}

相關文章