[ASP.NET Core 3框架揭祕] 配置[5]:配置資料與資料來源的實時同步
在《配置模型總體設計》介紹配置模型核心物件的時候,我們刻意迴避了與配置同步相關的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)來說,它們的GetReload
還有一點值得強調,IConfiguration介面的GetReloadToken方法返回的IChangeToken,其作用不是在配置源發生變化時嚮應用程式傳送通知,它實際上是通知應用程式:配置源已經發生改變,並且新的資料已經被相應的IConfigurationProvider重新載入進來。由於Configuration
Root和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]:自定義配置源