1. 程式人生 > >Asp.NetCore原始碼學習[1-2]:配置[Option]

Asp.NetCore原始碼學習[1-2]:配置[Option]

Asp.NetCore原始碼學習[1-2]:配置[Option]

在上一篇文章中,我們知道了可以通過IConfiguration訪問到注入的ConfigurationRoot,但是這樣只能通過索引器IConfiguration["配置名"]訪問配置。這篇文章將一下如何將IConfiguration對映到強型別。

本系列原始碼地址

一、使用強型別訪問Configuration的用法

指定需要配置的強型別MyOptions和對應的IConfiguration

public void ConfigureServices(IServiceCollection services)
{
    //使用Configuration配置Option
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
    //載入Configuration後再次進行配置
    services.PostConfigure<MyOptions>(options=> { options.FilePath = "/"; });
}

在控制器中通過DI訪問強型別配置,一共有三種方法可以訪問到強型別配置MyOptions,分別是IOptionsIOptionsSnapshotIOptionsMonitor。先大概瞭解一下這三種方法的區別:

public class ValuesController : ControllerBase
{
    private readonly MyOptions _options1;
    private readonly MyOptions _options2;
    private readonly MyOptions _options3;
    private readonly IConfiguration _configurationRoot;

    public ValuesController(IConfiguration configurationRoot, IOptionsMonitor<MyOptions> options1, IOptionsSnapshot<MyOptions> options2, 
        IOptions<MyOptions> options3 )
    {
        //IConfiguration(ConfigurationRoot)隨著配置檔案進行更新(需要IConfigurationProvider監聽配置源的更改)
        _configurationRoot = configurationRoot;
        //單例,監聽IConfiguration的IChangeToken,在配置源發生改變時,自動刪除快取
        //生成新的Option例項並繫結,加入快取
        _options1 = options1.CurrentValue;
        //scoped,每次請求重新生成Option例項並從IConfiguration獲取資料進行繫結
        _options2 = options2.Value;
        //單例,從IConfiguration獲取資料進行繫結,只繫結一次
        _options3 = options3.Value;
    }
}

二、原始碼解讀

首先看看Configure擴充套件方法,方法很簡單,通過DI注入了Options需要的依賴。這裡注入了了三種訪問強型別配置的方法所需的所有依賴,接下來我們按照這三種方法去分析原始碼。

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
    => services.Configure<TOptions>(Options.Options.DefaultName, config, _ => { });
    
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
    where TOptions : class
{
    services.AddOptions();
    
    services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
    
    return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}
/// 為IConfigurationSection例項註冊需要繫結的TOptions
public static IServiceCollection AddOptions(this IServiceCollection 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;
}

1. 通過IOptions訪問強型別配置

與其有關的注入只有三個:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));

services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));

services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));

從以上程式碼我們知道,通過IOptions訪問到的其實是OptionsManager例項。

1.1 OptionsManager 的實現

通過IOptionsFactory<>建立TOptions例項,並使用OptionsCache<>充當快取。OptionsCache<>實際上是通過ConcurrentDictionary實現了IOptionsMonitorCache介面的快取實現,相關程式碼沒有展示。

public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class
{
    private readonly IOptionsFactory<TOptions> _factory;

    // 單例OptionsManager的私有快取,通過ConcurrentDictionary實現了 IOptionsMonitorCache介面
    // Di中注入的單例OptionsCache<> 是給 OptionsMonitor<>使用的
    private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache

    public OptionsManager(IOptionsFactory<TOptions> factory)
    {
        _factory = factory;
    }

    public TOptions Value
    {
        get
        {
            return Get(Options.DefaultName);
        }
    }

    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
}

1.2 IOptionsFactory 的實現

首先通過Activator建立TOptions的例項,然後通過IConfigureNamedOptions.Configure()方法配置例項。該工廠類依賴於注入的一系列IConfigureOptions,在Di中注入的實現為NamedConfigureFromConfigurationOptions,其通過委託儲存了配置源和繫結的方法

/// Options工廠類 生命週期:Transient
/// 單例OptionsManager和單例OptionsMonitor持有不同的工廠例項
public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class
{
    private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
    private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
    {
        _setups = setups;
        _postConfigures = postConfigures;
    }

    public TOptions Create(string name)
    {
        var options = CreateInstance(name);
        foreach (var setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        foreach (var post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        return options;
    }

    protected virtual TOptions CreateInstance(string name)
    {
        return Activator.CreateInstance<TOptions>();
    }
}

1.3 NamedConfigureFromConfigurationOptions 的實現

在內部通過Action委託,儲存了IConfiguration.Bind()方法。該方法實現了從IConfigurationTOptions例項的賦值。
此處合併了NamedConfigureFromConfigurationOptionsConfigureNamedOptions的程式碼。

public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
    where TOptions : class
{
    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
        : this(name, config, _ => { })
    { }

    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
        : this(name, options => config.Bind(options, configureBinder))
    { }
    
    public ConfigureNamedOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
    }

    public string Name { get; }

    public Action<TOptions> Action { get; }

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

    public void Configure(TOptions options) => Configure(string.Empty, options);
}

由於OptionsManager<>是單例模式,只會從IConfiguration中獲取一次資料,在配置發生更改後,OptionsManager<>返回的TOptions例項不會更新。

2. 通過IOptionsSnapshot訪問強型別配置

該方法和第一種相同,唯一不同的是,在注入DI系統的時候,其生命週期為scoped,每次請求重新建立OptionsManager<>。這樣每次獲取TOptions例項時,會新建例項並從IConfiguration重新獲取資料對其賦值,那麼TOptions例項的值自然就是最新的。

3. 通過IOptionsMonitor訪問強型別配置

與其有關的注入有五個:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));

services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));

services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));

services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));

第二種方法在每次請求時,都新建例項進行繫結,對效能會有影響。如何監測IConfiguration的變化,在變化的時候進行重新獲取TOptions例項呢?答案是通過IChangeToken去監聽配置源的改變。從上一篇知道,當使用FileProviders監聽檔案更改時,會返回一個IChangeToken,在FileProviders中監聽返回的IChangeToken可以得知檔案發生了更改並進行重新載入檔案資料。所以使用IConfiguration 訪問到的ConfigurationRoot 永遠都是最新的。在IConfigurationProviderIConfigurationRoot中也維護了IChangeToken欄位,這是用於向外部一層層的傳遞更改通知。下圖為更改通知的傳遞方向:

graph LR
A["FileProviders"]--IChangeToken-->B
B["IConfigurationProvider"]--IChangeToken-->C["IConfigurationRoot"]

由於NamedConfigureFromConfigurationOptions沒有直接儲存IConfiguration欄位,所以沒辦法通過它獲取IConfiguration.GetReloadToken()。在原始碼中通過注入ConfigurationChangeTokenSource實現獲取IChangeToken的目的

3.1 ConfigurationChangeTokenSource的實現

該類儲存IConfiguration,並實現IOptionsChangeTokenSource介面

public class ConfigurationChangeTokenSource<TOptions> : IOptionsChangeTokenSource<TOptions>
{
    private IConfiguration _config;

    public ConfigurationChangeTokenSource(IConfiguration config) : this(string.Empty, config)
    { }

    public ConfigurationChangeTokenSource(string name, IConfiguration config)
    {
        _config = config;
        Name = name ?? string.Empty;
    }

    public string Name { get; }

    public IChangeToken GetChangeToken()
    {
        return _config.GetReloadToken();
    }
}

3.2 OptionsMonitor的實現

該類通過IOptionsChangeTokenSource獲取IConfigurationIChangeToken。通過監聽更改通知,在配置源發生改變時,刪除快取,重新繫結強型別配置,並加入到快取中。IOptionsMonitor介面還有一個OnChange()方法,可以註冊更改通知發生時候的回撥方法,在TOptions例項發生更改的時候,進行回撥。值得一提的是,該類有一個內部類ChangeTrackerDisposable,在註冊回撥方法時,返回該型別,在需要取消回撥時,通過ChangeTrackerDisposable.Dispose()取消剛剛註冊的方法。

    public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions>, IDisposable where TOptions : class
    {
        private readonly IOptionsMonitorCache<TOptions> _cache;
        private readonly IOptionsFactory<TOptions> _factory;
        private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
        private readonly List<IDisposable> _registrations = new List<IDisposable>();
        internal event Action<TOptions, string> _onChange;

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

        private void InvokeChanged(string name)
        {
            name = name ?? Options.DefaultName;
            _cache.TryRemove(name);
            var options = Get(name);
            if (_onChange != null)
            {
                _onChange.Invoke(options, name);
            }
        }

        public TOptions CurrentValue
        {
            get => Get(Options.DefaultName);
        }

        public virtual TOptions Get(string name)
        {
            name = name ?? Options.DefaultName;
            return _cache.GetOrAdd(name, () => _factory.Create(name));
        }

        public IDisposable OnChange(Action<TOptions, string> listener)
        {
            var disposable = new ChangeTrackerDisposable(this, listener);
            _onChange += disposable.OnChange;
            return disposable;
        }

        public void Dispose()
        {
            foreach (var registration in _registrations)
            {
                registration.Dispose();
            }

            _registrations.Clear();
        }

        internal class ChangeTrackerDisposable : IDisposable
        {
            private readonly Action<TOptions, string> _listener;
            private readonly OptionsMonitor<TOptions> _monitor;

            public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
            {
                _listener = listener;
                _monitor = monitor;
            }

            public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);

            public void Dispose() => _monitor._onChange -= OnChange;
        }
    }

4. 測試程式碼

本篇文章中,由於Option依賴於自帶的注入系統,而本專案中Di部分還沒有完成,所以,這篇文章的測試程式碼直接new依賴的物件。

public class ConfigurationTest
{
    public static void Run()
    {
        var builder = new ConfigurationBuilder();
        builder.AddJsonFile(null, $@"C:\WorkStation\Code\GitHubCode\CoreApp\CoreWebApp\appsettings.json", true,true);
        var configuration = builder.Build();
        Task.Run(() => {
            ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
                Console.WriteLine("Configuration has changed");
            });
        });
        var optionsChangeTokenSource = new ConfigurationChangeTokenSource<MyOption>(configuration);
        var configureOptions = new NamedConfigureFromConfigurationOptions<MyOption>(string.Empty, configuration);
        var optionsFactory = new OptionsFactory<MyOption>(new List<IConfigureOptions<MyOption>>() { configureOptions },new List<IPostConfigureOptions<MyOption>>());
        var optionsMonitor = new OptionsMonitor<MyOption>(optionsFactory,new List<IOptionsChangeTokenSource<MyOption>>() { optionsChangeTokenSource },new OptionsCache<MyOption>());
        optionsMonitor.OnChange((option,name) => {
            Console.WriteLine($@"optionsMonitor Detected Configuration has changed,current Value is {option.TestOption}");
        });
        Thread.Sleep(600000);
    }
}

測試結果

回撥會觸發兩次,這是由於FileSystemWatcher造成的,可以通過設定一個後臺執行緒,在檢測到檔案變化時,主執行緒將標誌位置true,後臺執行緒輪詢標誌位

---

結語

至此,從IConfigurationTOptions強型別的對映已經完成