文章出處

在《.NET Core采用的全新配置系統[1]: 讀取配置數據》中,我們通過實例的方式演示了幾種典型的配置讀取方式,其主要目的在于使讀者朋友們從編程的角度對.NET Core的這個全新的配置系統具有一個大體上的認識,接下來我們從設計的維度來重寫認識它。通過上面演示的實例我們知道,配置的編程模型涉及到三個核心對象,它們分別是ConfigurationConfigurationSourceConfigurationBuilder。如果從設計層面來審視這個配置系統,還缺少另一個名為ConfigurationProvider的核心對象,總得來說,.NET Core的這個配置模型由這四個核心對象組成。要徹底了解這四個核心對象之間的關系,我們先得來聊聊配置的幾種數據結構。 [ 本文已經同步到《ASP.NET Core框架揭秘》之中]

目錄
一、配置數據結構及其轉換
二、Configuration
三、ConfigurationProvider
四、ConfigurationSource
五、ConfigurationBuilder

一、配置數據結構及其轉換

相同的數據具有不同的表現和承載方式,同時體現出不同的數據結構。對于配置來說,它在被消費過程中是以Configuration對象的形式來體現的,該對象在邏輯上具有一個樹形化層次結構,所以我們可以稱之為配置樹,并將這棵樹視為配置的“邏輯結構”。

配置具有多種原始來源,可以是內存對象、物理文件、數據庫或者其他自定義的存儲介質,如果采用物理文件來存儲配置數據,我們還可以選擇不同的文件格式,常見的文件類型包括XML、JSON和INI三種,所以配置的原始數據結構是不確定的。配置模型的最終目的在于提取原始的配置數據并將其轉換成一個Configuration對象,話句話說,整個配置模型的使命就在于按照下圖所示的方式將配置數據從原始的結構轉換成樹形層次結構。

3

對于配置模型來說,配置從原始結構向邏輯結構的轉換不是一蹴而就的,在它們之間具有一種“中間結構”。話句話說,原始的配置數據被讀取出來之后會先統一轉換成這種中間結構的數據,那么這種中間結構到底是一種怎樣的數據結構呢?在《.NET Core采用的全新配置系統[1]: 讀取配置數據》我們說過,一棵配置樹通過其葉子結點承載所有的原子配置數據, 這棵樹的結構和承載的數據完全可以利用一個簡單的數據字典來表達。具體來說,我們只需要將所有葉子節點在配置樹種的路徑作為Key,將葉子結點承載的配置數據作為Value即可。所謂的“中間結構”指的就是這樣的數據字典,我們不妨將其稱為“物理結構”。所以配置模型會按照下圖所示的方式將具有不同原始結構的配置數據統一轉換成基于字典的物理結構,最終再完成針對邏輯結構的轉換。

4

對于配置模型的四個核心對象,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,配置模型分別定義了接口IConfigurationRootIConfigurationSection來表示它們,這兩個接口都是IConfiguration的繼承者。下圖為我們展示了由一個ConfigurationRoot對象和一組 ConfigurationSection對象構成的配置樹。

5

如下所示的是接口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按照如下圖所示的方式實現配置從原始結構向物理結構的轉換。

6

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展示了配置模型涉及的主要接口/類型以及它們之間的關系。

7


[1] 本小節提到的ConfigurationBuilder大部分情況下指代的是ConfigurationBuilder這個類型或者該類型的對象,而不是泛指所有實現了IConfigurationBulder接口的類型及其對應對象,。后面提到的ConfigurationRoot和ConfigurationSection也是這樣,請讀者朋友注意區分。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()