一 背景
相比.Net Framework , .NET Core的配置系統 ,有一些明顯的優點 ,如:
1 支持更豐富的配置源
2 讀取配置時, 可以用相同的方式讀取, 並且非常方便
3 修改配置后,不用重啟應用
本期對配置相關的源碼簡單梳理一下。 只說主邏輯 ,並且從用的角度說起 ,逐步深入。
二 配置讀取
2.1 舉個栗子
進入上代碼環節 ,新建控制台應用 , 只需要引入 "Microsoft.Extensions.Configuration
" 1個nuget包 ,就可以使用基本的配置了 。
(不得不說,相較於.NET Framework,.Net Core 的組件化設計實現的相當徹底 )
//數據源
var dicData = new Dictionary<string, string>() { { "Key1", "Value1" }, { "Key2", "Value2" } }; IConfigurationRoot configurationRoot = new ConfigurationBuilder() .Add(new MemoryConfigurationSource() { InitialData = dicData }) // 添加配置源 .Build(); // 構建配置 Console.WriteLine("Key1=" + configurationRoot["Key1"]);//輸出內容 Key1=Value1
我們好奇 configurationRoot["Key1"] 是怎么讀取到 "Value1" 呢 ? 這就需要看源碼了。 IConfigurationRoot
到底是什么 , 以及 ["key1"]
的代碼執行邏輯是怎樣的 。
(注意: 為了方便了解主線邏輯 , 本文的中 .Net Core 源碼有刪減 )
2.2 IConfigurationRoot
相關源碼
public interface IConfiguration { string this[string key] { get; set; } } public interface IConfigurationRoot : IConfiguration { IEnumerable<IConfigurationProvider> Providers { get; } } public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList<IConfigurationProvider> _providers; public IEnumerable<IConfigurationProvider> Providers => _providers; /// <summary> /// 根據指定的Key獲取或者設置對應的Value /// </summary> /// <param name="key">The configuration key.</param> /// <returns>The configuration value.</returns> public string this[string key] { get { for (var i = _providers.Count - 1; i >= 0; i--) { var provider = _providers[i]; if (provider.TryGet(key, out var value)) { return value; } } return null; } set { if (!_providers.Any()) { throw new InvalidOperationException(Resources.Error_NoSources); } foreach (var provider in _providers) { provider.Set(key, value); } } }
1 通過上面的代碼發現 ,IConfigurationRoot
有個 Providers
屬性 ;
2 讀取配置是通過ConfigurationRoot類中的索引器實現的 , 更具體的說就是通過 遍歷Providers ,調用其.TryGet(key, out var value)
獲取的 。
3 細心的同學會發現,一旦獲取到配置就結束並返回了。那么就有了從哪先獲取的問題 ,我們發現遍歷時是LIFO(后入先出)的順序,所以后面添加的Provider有更高的優先級 。
到目前為止 ,我們只需要記住一個主知識點 :獲取配置是由多個 Provider(供應者) provide (供應) 的 ,並且越后面的 Provider 優先級越高,具有覆蓋性。
再想深入就得研究一下 : IConfigurationProvider
是怎么回事 , 特別是其中的 TryGet
方法是怎么個邏輯 。
2.3 IConfigurationProvider
相關源碼
public interface IConfigurationProvider { bool TryGet(string key, out string value); void Load(); } public abstract class ConfigurationProvider : IConfigurationProvider { /// <summary> /// The configuration key value pairs for this provider. /// </summary> protected IDictionary<string, string> Data { get; set; } /// <summary> /// 根據指定的 鍵 獲取對應的 值, 獲取到返回true,否則返回 false /// </summary> public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value); /// <summary> /// 加載數據 /// </summary> public virtual void Load() { } }
代碼非常簡單 ,每個Provider 維護了一個 Data (字典類型) 屬性 , 配置就是從這個字典拿到的。 到此基本的讀取配置邏輯我們已經知道了 ,給自己點個贊吧 💪
2.4 使用便利性
這時 ,我們可以再看開頭那個栗子,讀取配置其實還有這種寫法 :
configurationRoot.Providers.First().TryGet("Key1", out string Val); Console.WriteLine("Key1=" + Val); //輸出 Val=Value1
但是這種寫法就感覺有些累贅了,還是 configurationRoot["Key1"] 更方便一些 , 有時我們甚至連"[" 和 "]" 都不想打 !
從使用的角度考慮,框架做了一些封裝,並提供了不少擴展方法。但是基本的讀取邏輯都是一樣的。
如果我們引入 "Microsoft.Extensions.Configuration.Binder
" nuget包 ,讀取配置操作時會發現一些新的方法。
比如配置和強模型綁定,並結合泛型使用 ,如:
public static T Get<T>(this IConfiguration configuration) public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue)
三 配置寫入
現在我們需要找Provider中的Data 屬性是怎么寫入的 ,還從開頭的控制台程序入手 , 其中有這樣一段代碼 :
new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = dicData }).Build();
我們重點看Add
方法
3.1 IConfigurationBuilder 相關源碼
public interface IConfigurationBuilder { IList<IConfigurationSource> Sources {get;} IConfigurationBuilder Add(IConfigurationSource source); IConfigurationRoot Build(); } /// <summary> /// Used to build key/value based configuration settings for use in an application. /// </summary> public class ConfigurationBuilder : IConfigurationBuilder { /// <summary> /// Returns the sources used to obtain configuration values. /// </summary> public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>(); /// <summary> /// Adds a new configuration source. /// </summary> /// <param name="source">The configuration source to add.</param> /// <returns>The same <see cref="IConfigurationBuilder"/>.</returns> public IConfigurationBuilder Add(IConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Sources.Add(source); return this; } /// <summary> /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in /// <see cref="Sources"/>. /// </summary> /// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns> public IConfigurationRoot Build() { var providers = new List<IConfigurationProvider>(); foreach (var source in Sources) { var provider = source.Build(this); providers.Add(provider); } return new ConfigurationRoot(providers); } }
Add就是把配置源 Source ,添加到內部的Sources列表。IConfigurationBuilder 除了Add方法還有一個 Build 方法。
IConfigurationBuilder 就像一個車間 ,Add一堆零件后 ,Build一下, 整出來一輛 汽車 (IConfigurationRoot) , 很明顯IConfigurationBuilder是一個構建者模式 。
我們繼續探索 IConfigurationSource ,看 Build
方法的實現邏輯 .
3.2 IConfigurationSource 相關源碼
public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); } public class MemoryConfigurationSource : IConfigurationSource { /// <summary> /// The initial key value configuration pairs. /// </summary> public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; } /// <summary> /// Builds the <see cref="MemoryConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="MemoryConfigurationProvider"/></returns> public IConfigurationProvider Build(IConfigurationBuilder builder) { return new MemoryConfigurationProvider(this); } }
可以看到 ,IConfigurationSource 只干了一件事 :其 Build 方法返回一個Provider。
還拿開頭的栗子來講 ,MemoryConfigurationSource.Build 方法返回了 一個新的 MemoryConfigurationProvider 實例。
再看下 MemoryConfigurationProvider
public class MemoryConfigurationProvider : ConfigurationProvider, IEnumerable<KeyValuePair<string, string>> { private readonly MemoryConfigurationSource _source; /// <summary> /// Initialize a new instance from the source. /// </summary> /// <param name="source">The source settings.</param> public MemoryConfigurationProvider(MemoryConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } _source = source; if (_source.InitialData != null) { foreach (var pair in _source.InitialData) { Data.Add(pair.Key, pair.Value); } } } /// <summary> /// Add a new key and value pair. /// </summary> /// <param name="key">The configuration key.</param> /// <param name="value">The configuration value.</param> public void Add(string key, string value) { Data.Add(key, value); } }
MemoryConfigurationProvider 的構造函數完成了數據的初始化 。
到此為止,開頭的那個栗子我們就梳理完了。
也許是我們開頭那個例子過於簡單了 , 寫配置這塊有點虎頭蛇尾的感覺 。
我們再看一下主角 IConfigurationProvider , 發現其中定義的 Load 方法 ,我們還知道 ConfigurationProvider 是一個虛擬類 ,其 Load 方法還是個虛方法。
我們在翻一下源碼看有沒有其它類繼承它呢 ? 如果有的話 ,它們是不是覆蓋了 Load 方法呢?
果然我們發現了 不少 Provider , 如 JsonConfigurationProvider,XmlConfigurationProvider,EnvironmentVariablesConfigurationProvider 等。
為方便講解,我們以EnvironmentVariablesConfigurationProvider 為例,並精簡了代碼。 Load 方法負責了負載配置的任務。男團其他成員也是如此。所以說 Provider們 實力擔當。
public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider { private readonly string _prefix; /// <summary> /// Initializes a new instance. /// </summary> public EnvironmentVariablesConfigurationProvider() : this(string.Empty) { } /// <summary> /// Initializes a new instance with the specified prefix. /// </summary> /// <param name="prefix">A prefix used to filter the environment variables.</param> public EnvironmentVariablesConfigurationProvider(string prefix) { _prefix = prefix ?? string.Empty; } /// <summary> /// Loads the environment variables. /// </summary> public override void Load() { Load(Environment.GetEnvironmentVariables()); } internal void Load(IDictionary envVariables) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var filteredEnvVariables = envVariables .Cast<DictionaryEntry>() .Where(entry => ((string)entry.Key).StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)); foreach (var envVariable in filteredEnvVariables) { var key = ((string)envVariable.Key).Substring(_prefix.Length); data[key] = (string)envVariable.Value; } Data = data; } }
但是 IConfigurationSource 呢 ,是不是可有可無 ?其實不是。IConfigurationSource 作用是提供原始配置,並將必要的信息傳遞給 Provider 。
我們現在大概知道 IConfigurationBuilder , IConfigurationProvider , IConfigurationRoot ,IConfigurationSource 之間的關系。我們總結一下,
1 各種配置原始數據( IConfigurationSource) 通過 IConfigurationProvider 轉成鍵值對格式的配置數據
2 IConfigurationBuilder 內部維護多個 IConfigurationSource,IConfigurationBuilder 中的 Build方法 構建出 IConfigurationRoot
3 IConfigurationRoot 內部維護多個 IConfigurationProvider,並通過 IConfigurationProvider 讀取配置數據
闖盪天下的天外流星全套劍譜奉上
限於篇幅 ,本篇介紹 配置讀取 ,配置寫入 2個知識點。
下一篇會介紹 配置變更監控 ,自定義配置 2個知識點。
感覺寫的還湊合就點個贊吧 😊