.NET Core採用的全新配置系統[2]: 配置模型設計詳解
在《.NET Core採用的全新配置系統[1]: 讀取配置資料》中,我們通過例項的方式演示了幾種典型的配置讀取方式,其主要目的在於使讀者朋友們從程式設計的角度對.NET Core的這個全新的配置系統具有一個大體上的認識,接下來我們從設計的維度來重寫認識它。通過上面演示的例項我們知道,配置的程式設計模型涉及到三個核心物件,它們分別是Configuration、ConfigurationSource和ConfigurationBuilder。如果從設計層面來審視這個配置系統,還缺少另一個名為ConfigurationProvider的核心物件,總得來說,.NET Core的這個配置模型由這四個核心物件組成。要徹底瞭解這四個核心物件之間的關係,我們先得來聊聊配置的幾種資料結構。 [ 本文已經同步到《
目錄
一、配置資料結構及其轉換
二、Configuration
三、ConfigurationProvider
四、ConfigurationSource
五、ConfigurationBuilder
一、配置資料結構及其轉換
相同的資料具有不同的表現和承載方式,同時體現出不同的資料結構。對於配置來說,它在被消費過程中是以Configuration物件的形式來體現的,該物件在邏輯上具有一個樹形化層次結構,所以我們可以稱之為配置樹,並將這棵樹視為配置的“邏輯結構”。
配置具有多種原始來源,可以是記憶體物件、物理檔案、資料庫或者其他自定義的儲存介質,如果採用物理檔案來儲存配置資料,我們還可以選擇不同的檔案格式,常見的檔案型別包括XML、JSON和INI三種,所以配置的原始資料結構是不確定的。配置模型的最終目的在於提取原始的配置資料並將其轉換成一個Configuration物件,話句話說,整個配置模型的使命就在於按照下圖所示的方式將配置資料從原始的結構轉換成樹形層次結構。
對於配置模型來說,配置從原始結構向邏輯結構的轉換不是一蹴而就的,在它們之間具有一種“中間結構”。話句話說,原始的配置資料被讀取出來之後會先統一轉換成這種中間結構的資料,那麼這種中間結構到底是一種怎樣的資料結構呢?在《.NET Core採用的全新配置系統[1]: 讀取配置資料》我們說過,一棵配置樹通過其葉子結點承載所有的原子配置資料, 這棵樹的結構和承載的資料完全可以利用一個簡單的資料字典來表達。具體來說,我們只需要將所有葉子節點在配置樹種的路徑作為Key,將葉子結點承載的配置資料作為Value即可。所謂的“中間結構”指的就是這樣的資料字典,我們不妨將其稱為“物理結構”。所以配置模型會按照下圖所示的方式將具有不同原始結構的配置資料統一轉換成基於字典的物理結構,最終再完成針對邏輯結構的轉換。
對於配置模型的四個核心物件,Configuration對配置樹的體現,其他三個(ConfigurationSource、ConfigurationBuilder和ConfigurationProvider)在配置的結構轉換過程中扮演著不同的角色,至於它們究竟起到怎樣的作用,我們將在接下來的內容中對它們作專門的介紹。
二、Configuration
配置在應用程式中總是以一個Configuration物件的形式供我們使用,我們所說的Configuration是對所有實現了IConfiguration介面的所有型別一起對應物件的統稱。一個Configuration物件具有樹形層次化結構的意思並不是說對應的型別具有對應的資料成員(欄位或者屬性)定義,而是說它提供的API在邏輯上體現出樹形化層次結構,所以我們才說配置樹是一種邏輯結構。如下所示的是IConfiguration介面的完整定義,所謂的層次化邏輯結構就體現在它的成員定義上。
1: public interface IConfiguration
2: {
3: IEnumerable<IConfigurationSection> GetChildren();
4: IConfigurationSection GetSection(string key);
5: IChangeToken GetReloadToken();
6:
7: string this[string key] { get; set; }
8: }
一個Configuration物件表示配置樹的某個配置節點。對於組成整棵樹的所有配置節點來說,表示根節點的Configuration物件與表示其它配置節點的Configuration物件是不同的,所以配置模型採用不同的介面來表示它們。具體來說,根節點所在的Configuration物件被稱為ConfigurationRoot,除此之外的其他Configuration物件則被稱為ConfigurationSection,配置模型分別定義了介面IConfigurationRoot和IConfigurationSection來表示它們,這兩個介面都是IConfiguration的繼承者。下圖為我們展示了由一個ConfigurationRoot物件和一組 ConfigurationSection物件構成的配置樹。
如下所示的是介面IConfigurationRoot的定義,可見該介面僅僅唯一的方法Reload實現對配置資料的重新載入。ConfigurationRoot物件表示的配置樹的根,也可以是它根本就是對整棵配置樹的體現,如果如果它被重新載入了,意味著整棵配置樹承載的所有配置資料均被重新載入了。
1: public interface IConfigurationRoot : IConfiguration
2: {
3: void Reload();
4: }
表示非根配置節點的IConfigurationSection介面具有如下三個屬性,只讀屬性Key用來唯一標識多個具有相同父節點的ConfigurationSection物件,而Path則表示當前配置節點在配置樹中的路徑,該路徑由ConfigurationSection的Key組成,並採用冒號(“:”)作為分隔符。Path和Key的組合體現了當前配置節在整個配置樹中的位置。
1: public interface IConfigurationSection : IConfiguration
2: {
3: string Path { get; }
4: string Key { get; }
5: string Value { get; set; }
6: }
IConfigurationSection的Value屬性表示配置節點承載的配置資料。在大部分情況下,只有配置樹的葉子節點對應的ConfigurationSection物件才具有值,非葉子節點對應的ConfigurationSection物件實際上僅僅表示存放所有子配置節點的邏輯容器,它們的Value一般返回Null。值得一體的是,這個Value屬性並不是只讀的,而是可讀可寫的,但是我們寫入的值一般不會被持久化,所以以來配置樹被重新載入,寫入的值將會丟失。
在對ConfigurationRoot和ConfigurationSection具有基本瞭解情況下我們回過頭來看看定義在介面IConfiguration中的成員。它的GetChildren方法返回的ConfigurationSection集合表示率屬於它的所有自配置節點,另一個方法GetSection則根據指定的Key得到一個具體的子配置節點。當GetSection方法執行的時候,指定的引數將會與當前ConfigurationSection的Path進行組合以確定目標配置節點所在的路徑,所以如果在呼叫該方法的時候指定一個相對於當前配置節的路徑,我們是可以得到子節點以下的某個配置節。
1: Dictionary<string, string> source = new Dictionary<string, string>
2: {
3: ["A:B:C"] = "ABC"
4: };
5: IConfiguration root = new ConfigurationBuilder()
6: .Add(new MemoryConfigurationSource { InitialData = source })
7: .Build();
8:
9: IConfigurationSection section1 = root.GetSection("A:B:C");
10: IConfigurationSection section2 = root.GetSection("A:B").GetSection("C");
11: IConfigurationSection section3 = root.GetSection("A").GetSection("B:C");
12:
13: Debug.Assert(section1.Value == "ABC");
14: Debug.Assert(section2.Value == "ABC");
15: Debug.Assert(section3.Value == "ABC");
16:
17: Debug.Assert(!ReferenceEquals(section1, section2));
18: Debug.Assert(!ReferenceEquals(section1, section3));
19: Debug.Assert(null != root.GetSection("D"));
如上面的程式碼片段所示,我們以不同的方式呼叫GetSection方法得到的都是路徑為“A:B:C”的ConfigurationSection。上面這段程式碼還體現了另一個有趣的現象,雖然這三個ConfigurationSection物件均指向配置樹的同一個節點,但是它們卻並非同一個物件。換句話說,當我們呼叫GetSection方法的時候,不論配置樹種是否存在一個與指定路徑匹配的配置節,它總是會建立一個ConfigurationSection物件。
IConfiguration還具有一個索引,我們可以指定子配置節的Key或者相對當前配置節點的路徑得到對應ConfigurationSection的值。當這個索引執行的時候,它會按照與GetSection方法完全一致的邏輯得到一個ConfigurationSection物件,並返回其Value屬性。如果配置樹中不具有匹配的配置節,該索引會返回Null而不會丟擲異常。
三、ConfigurationProvider
在第一節介紹ConfigurationSource物件時,我們說它對原始配置源的體現。雖然每種不同型別的配置源都具有一個對應的ConfigurationSource型別,但是針對原始資料的讀取並不由ConfigurationSource來提供,而是委託一個對應的ConfigurationProvider物件來完成。在上面介紹的配置結構轉換過程中,針對不同配置源型別的ConfigurationProvider按照如下圖所示的方式實現配置從原始結構向物理結構的轉換。
ConfigurationProvider是對所有實現了IConfigurationProvider介面的所有型別以及對應物件的統稱。由於ConfigurationProvider的目的在於將配置從原始結構轉換成物理結構,配置資料的物理結構體現為一個簡單的二維資料字典,所以我們會發現定義在IConfigurationProvider介面中的方法大都體現為針對字典物件的相關操作。
1: public interface IConfigurationProvider
2: {
3: void Load();
4:
5: bool TryGet(string key, out string value);
6: void Set(string key, string value);
7: IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
8: }
配置資料的載入通過呼叫ConfigurationProvider的Load方法來完成。我們可以呼叫TryGet方法獲取由指定的Key所標識的配置項的值。從資料持久化的角度來講,ConfigurationProvider基本上都是隻讀的,也就是說ConfigurationProvider只負責從持久化資源中讀取配置資料,而不負責更新儲存在持久化資源的配置資料,所以它提供的Set方法設定的配置資料一般只會儲存在記憶體中。ConfigurationProvider的GetChildKeys方法用於獲取某個指定配置節點的所有子節點的Key。
每種型別的配置源都具有對應的ConfigurationProvider型別,這些型別一般不會直接實現介面IConfigurationProvider,而會選擇繼承另一個名為ConfigurationProvider的抽象類。這個抽象類的定義其實很簡單,從如下的程式碼片段可以看出它僅僅是對一個IDictionary<string, string>物件(Key不區分大小寫)的封裝,其Set和TryGetValue方法最終操作的都是這個字典物件。它實現了Load方法並將其定義成虛方法,具體的ConfigurationProvider可以通過重寫這個方法從相應的資料來源中讀取配置資料並對這個字典物件進行初始化。
1: public abstract class ConfigurationProvider : IConfigurationProvider
2: {
3: protected IDictionary<string, string> Data { get; set; }
4:
5: public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
6: {
7: //省略實現
8: }
9:
10: public virtual void Load()
11: {}
12:
13: public void Set(string key, string value)
14: {
15: this.Data[key] = value;
16: }
17:
18: public bool TryGet(string key, out string value)
19: {
20: return this.Data.TryGetValue(key, out value);
21: }
22: //其他成員
23: }
四、ConfigurationSource
ConfiurationSource在配置模型中代表配置源,它通過註冊到ConfigurationBuilder上為後者建立的Configuration提供原始的配置資料。由於針對原始配置資料的讀取實現在相應的ConfigurationProvider之中,所以ConfigurationSource所起的作用在於提供相應的ConfigurationProvider。ConfigurationSource是對所有實現了IConfigurationSource介面的所有型別及其物件的統稱,如下面的程式碼片段所示,該介面具有一個唯一的Build方法根據指定的ConfigurationBuilder物件提供對應的ConfigurationProvider。
1: public interface IConfigurationSource
2: {
3: IConfigurationProvider Build(IConfigurationBuilder builder);
4: }
五、ConfigurationBuilder
ConfigurationBulder在整個配置模型中處於一個核心地位,它是Configuration的建立者,代表原始配置源的ConfigurationSource也註冊到它上面。ConfigurationBulder是對所有實現了IConfigurationBulder介面的所有型別及其對應物件的統稱。如下面的程式碼片段所示,IConfigurationBulder介面定義了兩個方法,其中Add方法用於註冊ConfigurationSource,最終的Configuration則通過Build方法建立,後者返回一個代表整棵配置的數的ConfigurationRoot物件。註冊的ConfigurationSource被儲存在通過Sources屬性表示的集合中,而另一個屬性Properties則以字典的形式存放任意的自定義屬性。
1: public interface IConfigurationBuilder
2: {
3: IEnumerable<IConfigurationSource> Sources { get; }
4: Dictionary<string, object> Properties { get; }
5:
6: IConfigurationBuilder Add(IConfigurationSource source);
7: IConfigurationRoot Build();
8: }
配置系統提供了一個名為ConfigurationBulder[1]的類作為IConfigurationBulder介面的預設實現者。定義在它上面的Build方法體現了配置系統讀取原始配置資料並生成配置樹的預設機制,這是我們接下來重點講述的內容。ConfigurationBulder類的Build方法返回一個型別為ConfigurationRoot的物件,對於一個通過該物件表示配置樹來說,每個非根配置節點均是一個型別為ConfigurationSection的物件,這兩個型別(ConfigurationRoot和ConfigurationSection)自然是IConfigurationRoot和IConfigurationSection介面的實現者。
ConfigurationRoot代表著一顆完整的配置樹,但是不論是這個物件本身,還是表示這棵樹非根配置節點的ConfigurationSection物件,它們自身都沒有維護任何的資料。這句話好似顯得自相矛盾,但實則不然,因為所謂的配置樹僅僅是API在邏輯上所體現的資料結構,並不是具體的配置資料也是按照這樣的結構進行儲存的。由於這兩個物件均不作任何的資料封裝,針對它們的資料提取請求最終都會交給一組ConfigurationProvider來完成,後者自然就是註冊到ConfigurationBuilder上的這組ConfigurationSource所提供的ConfigurationProvider。
本節內容從設計和實現原理的角度對配置模型進行了詳細的介紹。總的來說,配置模型涉及到四個核心物件,包括承載配置邏輯結構的Configuration物件和它的建立者ConfigurationBuilder,以及與配置源相關的ConfigurationSource和ConfigurationProvider。這四個核心物件之間的關係簡單而清晰,完全可以通過一句話來概括:ConfigurationBuilder利用註冊的ConfigurationSource來提供的ConfigurationProvider讀取原始配置資料並創建出相應的Configuration物件。下圖所示的UML展示了配置模型涉及的主要介面/型別以及它們之間的關係。
[1] 本小節提到的ConfigurationBuilder大部分情況下指代的是ConfigurationBuilder這個型別或者該型別的物件,而不是泛指所有實現了IConfigurationBulder介面的型別及其對應物件,。後面提到的ConfigurationRoot和ConfigurationSection也是這樣,請讀者朋友注意區分。