在《配置模型總體設計》介紹配置模型核心對象的時候,我們刻意回避了與配置同步相關的API,現在我們利用一個獨立文章來專門討論這個話題。配置的同步涉及到兩個方面:第一,對原始的配置源實施監控並在其發生變化之后重新加載配置;第二,配置重新加載之后及時通知應用程序進而使應用能夠及時使用最新的配置。要了解配置同步機制的實現原理,我們先得了解一下配置數據的流向。
一、配置數據流
通過前面的介紹,我們已經對配置模型有了充分的了解,處於核心地位的 IConfigurationBuilder對象借助注冊的IConfigurationSource對象提供的IConfigurationProvider對象從相應的配置源中加載數據,而各種針對IConfigurationProvider接口的實現就是為了將形態各異的原始配置數據轉換成配置字典。我們在應用程序中使用的配置數據直接來源於IConfigurationBuilder對象創建的IConfiguration對象,那么當我們調用定義在IConfiguration對象上的API獲取配置數據時,配置數據究竟具有怎樣的流向呢?
我們在前面已經提到過,由ConfigurationBuilder(IConfigurationBuilder接口的默認實現)的Build方法提供的IConfiguration對象是一個ConfigurationRoot對象,它代表着整顆配置樹,而組成這棵樹的配置節則通過ConfigurationSection對象表示。這棵由ConfigurationRoo對象表示的配置樹其實是無狀態的,也就說不論是ConfigurationRoot對象還是ConfigurationSection對象,它們並沒有利用某個字段存儲任何的配置數據。
ConfigurationRoot對象保持着對所有注冊IConfigurationSource提供的IConfigurationProvider對象的引用,當我們調用ConfigurationRoot或者ConfigurationSection相應的API提取配置數據時,最終都會直接從這些IConfigurationProvider中提取數據。換句話說,配置數據在整個模型中只以配置字典的形式存儲在IConfigurationProvider對象上面。
應用程序在讀取配置時產生的數據流基本體現在上圖中。接下來我們從ConfigurationRoot和ConfigurationSection這兩個類型的定義來對這個數據流,以及建立在此基礎上的配置同步機制作進一步的介紹,不過在這之前我們得先來了解一個名為ConfigurationReloadToken的類型。
二、ConfigurationReloadToken
ConfigurationRoot和ConfigurationSection的GetReloadToken方法返回的IChangeToken對象類型都是ConfigurationReloadToken。不僅如此,對於組成同一棵配置樹的所有節點對應的IConfiguration對象(ConfigurationRoot或者ConfigurationSection)來說,它們的GetReloadToken方法返回的其實是同一個ConfigurationReloadToken對象。
還有一點值得強調,IConfiguration接口的GetReloadToken方法返回的IChangeToken,其作用不是在配置源發生變化時向應用程序發送通知,它實際上是通知應用程序:配置源已經發生改變,並且新的數據已經被相應的IConfigurationProvider重新加載進來。由於ConfigurationRoot和ConfigurationSection對象都不維護任何數據,它們僅僅將我們的API調用轉移到IConfigurationProvider對象上,所以應用程序使用原來的IConfiguration對象就可以獲取到最新的配置數據。
ConfigurationReloadToken本質上是對一個CancellationTokenSource對象的封裝。從如下的代碼片段可以看出,ConfigurationReloadToken與CancellationChangeToken具有類似的定義和實現。兩者唯一不同之處在於:CancellationChangeToken對象利用創建時提供的CancellationTokenSource對象對外發送通知,而ConfigurationReloadToken對象則通過調用OnReload方法利用內置的CancellationTokenSource對象發送通知。
public class ConfigurationReloadToken : IChangeToken { private CancellationTokenSource _cts = new CancellationTokenSource(); public IDisposable RegisterChangeCallback(Action<object> callback, object state) =>_cts.Token.Register(callback, state); public bool ActiveChangeCallbacks => True; public bool HasChanged =>_cts.IsCancellationRequested; public void OnReload() => _cts.Cancel(); }
三、ConfigurationRoot對象
接下來我們來看看由ConfigurationBuilder對象的Build方法直接創建的ConfigurationRoot對象具有怎樣的實現。正如我們前面所說,一個ConfigurationRoot對象根據一組IConfigurationProvider對象創建,這些IConfigurationProvider對象則由注冊的IConfigurationSource對象來提供。
public class ConfigurationRoot : IConfigurationRoot { private IList<IConfigurationProvider> _providers; private ConfigurationReloadToken _changeToken; public ConfigurationRoot(IList<IConfigurationProvider> providers) { _providers = providers; _changeToken = new ConfigurationReloadToken(); foreach (var provider in providers) { provider.Load(); ChangeToken.OnChange( () => provider.GetReloadToken(), () => RaiseChanged()); } } public void Reload() { foreach (var provider in _providers) { provider.Load(); } RaiseChanged(); } public IChangeToken GetReloadToken() => _changeToken; private void RaiseChanged() => Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()) .OnReload(); ... }
ConfigurationRoot的GetReloadToken方法返回的是一個ConfigurationReloadToken對象,該對象通過字段_changeToken表示。我們知道如果需要利用這個對象對外發送配置重新加載的通知,需要調用其OnReload方法就可以了,通過上面的代碼片段我們知道該方法會在RaiseChanged方法中被調用。由於一個IChangeToken對象只能發送一次通知,所以該方法還負責創建新的ConfigurationReloadToken對象並對_changeToken字段賦值。
換句話說,一旦ConfigurationRoot的RaiseChanged方法被調用,我們就可以利用其GetReloadToken方法返回的IChangeToken對象接收到配置被重新加載的通知。通過上面提供的代碼,我們可以看到這個RaiseChanged方法在兩個地方被調用:第一,在構造函數中調用每個IConfigurationProvider對象的GetReloadToken方法得到對應的IChangeToken對象后,並為它們注冊的回調中調用了這個方法;第二,實現的Reload方法依次調用每個IConfigurationProvider對象的Load方法重新加載配置數據之后,調用了這個RaiseChanged方法。按照這個邏輯,應用程序會在如下兩個場景中利用ConfigurationRoot返回的IChangeToken接收到配置被重新加載的通知:
- 某個IConfigurationProvider對象捕捉到對應配置源的改變后自動重新加載配置,並在加載完成后利用其GetReloadToken方法返回的IChangeToken發送通知;
- 我們顯式調用ConfigurationRoot的Reload方法手動加載配置。
在了解了ConfigurationRoot的GetRealodToken返回的是什么樣的IChangeToken之后,我們接着介紹它的其他成員具有怎樣的實現 。如下面的代碼片段所示,在ConfigurationRoot的索引定義中,它分別調用了IConfigurationProvider對象的TryGet和Set方法根據配置字典的Key獲取和設置對應的Value。
public class ConfigurationRoot : IConfigurationRoot { private IList<IConfigurationProvider> _providers; public string this[string key] { get { foreach (var provider in _providers.Reverse()) { if (provider.TryGet(key, out var value)) { return value; } } return null; } set { foreach (var provider in _providers) { provider.Set(key, value); } } } public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); public IEnumerable<IConfigurationSection> GetChildren() => GetChildrenImplementation(null); internal IEnumerable<IConfigurationSection> GetChildrenImplementation( string path) { return _providers .Aggregate(Enumerable.Empty<string>(), (seed, source) => source.GetChildKeys(seed, path)) .Distinct() .Select(key => GetSection(path == null ? key : $"{path}:{key}")); } public IEnumerable<IConfigurationProvider> Providers => _providers; }
從索引的定義可以看出,ConfigurationRoot在讀取Value值時針對IConfigurationProvider列表的遍歷是從后往前的,這一點非常重要,因為該特性決定了IConfigurationSource的注冊會采用“后來居上”的原則。也就說如果多個IConfigurationSource配置源提供的IConfiguationProvider對象包含同名的配置項,后面注冊的IConfigurationSource對象具有更高選擇優先級,我們應該根據這個特性合理安排IConfigurationSource對象的注冊順序。在進行Value的設置的時候,ConfigurationRoot對象會調用每個IConfigurationProvider對象的Set方法,這意味着新的值會被保存到所有IConfigurationProvider對象的配置字典中。
正如我們前面多次提到過的,通過ConfigurationRoot表示的配置樹的所有配置節都是一個類型為ConfigurationSection的對象,這一點體現在實現的GetSection方法上。將對應的路徑作為參數,我們可以得到組成配置樹的所有配置節。用於獲取所有子配置節的GetChildren方法通過調用內部方法GetChildrenImplementation來實現。GetChildrenImplementation方法旨在獲取配置樹某個節點的所有子節點,該方法的參數表示指定節點針對配置樹根的路徑。當這個方法被執行的時候,它會以聚合的形式遍歷所有的IConfigurationProvider並調用它們的GetChildKeys方法獲取所有子節點的Key,這些Key與當前節點的路徑進行合並后代表子節點的路徑,這些路徑最終被作為參數調用GetSection方法創建出對應的配置節。
四、ConfigurationSection對象
如下所示的代碼片段大體上體現了代表配置節的ConfigurationSection類型的實現邏輯。如下面的代碼片段所示,一個ConfigurationSection對象通過代表配置樹根的ConfigurationRoot對象和當前配置節在配置樹中的路徑來構建。ConfigurationSection的Path屬性直接返回構建時指定的路徑,而Key屬性則由根據這個路徑解析出來 。
public class ConfigurationSection : IConfigurationSection { private readonly ConfigurationRoot _root; private readonly string _path; private string _key; public ConfigurationSection(ConfigurationRoot root, string path) { _root = root; _path = path; } public string this[string key] { get => _root[string.Join(':', new string[] { _path, _key })]; set => _root[string.Join(':', new string[] { _path, _key })] = value; } public string Key => _key ?? (_key = _path.Contains(':') ? _path.Split(':').Last() : _path); public string Path => _path; public string Value { get => _root[_path]; set => _root[_path] = value; } public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(_path); public IChangeToken GetReloadToken() => _root.GetReloadToken(); public IConfigurationSection GetSection(string key) => _root.GetSection(string.Join(':', new string[] { _path, key })); }
如下圖6-15所示,實現在ConfigurationSection類型中的大部分成員都是調用ConfigurationRoot對象相應的API來實現的。ConfigurationSection的索引直接調用ConfigurationRoot的索引來獲取或者設置配置字典的Value,GetChildren方法返回的就是調用GetChildrenImplementation方法得到的結果,而GetReloadToken和GetSection方法都是通過調用同名方法實現的。
[ASP.NET Core 3框架揭秘] 配置[1]:讀取配置數據[上篇]
[ASP.NET Core 3框架揭秘] 配置[2]:讀取配置數據[下篇]
[ASP.NET Core 3框架揭秘] 配置[3]:配置模型總體設計
[ASP.NET Core 3框架揭秘] 配置[4]:將配置綁定為對象
[ASP.NET Core 3框架揭秘] 配置[5]:配置數據與數據源的實時同步
[ASP.NET Core 3框架揭秘] 配置[6]:多樣化的配置源[上篇]
[ASP.NET Core 3框架揭秘] 配置[7]:多樣化的配置源[中篇]
[ASP.NET Core 3框架揭秘] 配置[8]:多樣化的配置源[下篇]
[ASP.NET Core 3框架揭秘] 配置[9]:自定義配置源