文章出處

物理文件是我們最常用到的原始配置的載體,最佳的配置文件格式主要由三種,它們分別是JSON、XML和INI,對應的配置源類型分別是JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource。 [ 本文已經同步到《ASP.NET Core框架揭秘》之中]

目錄
一、FileConfigurationSource  & FileConfigurationProvider
二、JsonConfigurationSource &JsonConfigurationProvider
三、XmlConfiguationSource & XmlConfiguationProvider
四、IniConfigurationSource & IniConfigurationSource

一、FileConfigurationSource  & FileConfigurationProvider

上述這三個具體的ConfigurationSource類型具有如下一個相同的基類FileConfigurationSource。

   1: public abstract class FileConfigurationSource : IConfigurationSource
   2: {
   3:     public IFileProvider     FileProvider { get; set; }
   4:     public bool              Optional { get; set; }
   5:     public string            Path { get; set; }
   6:     public bool              ReloadOnChange { get; set; }
   7:  
   8:     public abstract IConfigurationProvider Build(IConfigurationBuilder builder);
   9: }

如上面的代碼片段所示,FileConfigurationSource是一個抽象類,它利用一個FileProvider對象來提供原始的配置文件,而Path屬性自然代表配置文件的路徑。Optional屬性表示明當前的FileConfigurationSource是否是可選的配置源,其默認值為False。當某個FileConfigurationSource的Optional屬性為True的時候,如果指定的配置文件路徑不存在,將不會有任何異常被拋出來。由于FileProvider具有監控文件變化的能力,它的ReloadOnChange屬性表示如果被監控的配置文件發生改變后是否需要重新加載配置。

由于FileConfigurationSource總是利用FileProvider來讀取配置文件的內容,所以當我們創建一個具體的FileConfigurationSource對象的時候都需要采用顯式或者隱式的方式指定一個FileProvider對象(關于FileProvider,可以參閱我的“文件系統”博文系列)。我們可以調用擴展方法SetFileProvider將一個默認的FileProvider注冊到ConfigurationBuilder對象上,從相面的代碼片段可以看出注冊的FileProvider被保存到Properties屬性表示的字典對象上,對應的Key為“FileProvider”。

   1: public static class FileConfigurationExtensions
   2: {
   3:     private static string FileProviderKey = "FileProvider";
   4:  
   5:     public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
   6:     {
   7:         object obj2;       
   8:         if (builder.Properties.TryGetValue(FileProviderKey, out obj2))
   9:         {
  10:             return (builder.Properties[FileProviderKey] as IFileProvider);
  11:         }
  12:         return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
  13:     }
  14:  
  15:     public static IConfigurationBuilder SetBasePath(this IConfigurationBuilder builder, string basePath)
  16:     {       
  17:         return builder.SetFileProvider(new PhysicalFileProvider(basePath));
  18:     }
  19:  
  20:     public static IConfigurationBuilder SetFileProvider(this IConfigurationBuilder builder, IFileProvider fileProvider)
  21:     {        
  22:         builder.Properties[FileProviderKey] = fileProvider;
  23:         return builder;
  24:     }
  25: }

通過隱式方式提供的FileProvider通過調用ConfigurationBuilder的另一個擴展方法GetFileProvider方法獲取。從上面給出的代碼片段我們可以看到,它會優先返回我們注冊的FileProvider。如果這樣的FileProvdier尚未注冊,該方法會返回指向當前應用執行目錄的PhysicalFileProvider對象。除了上述這兩個方法,ConfigurationBuilder還具有另一個名為SetBasePath的方法,該方法采用指定的路徑創建一個PhysicalFileProvider對象并對它進行注冊。

雖然JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource這些針對通過文件類型的ConfigurationSource會提供不同類型的ConfigurationProvider來讀取對應的配置文件并將讀取的內容轉換成一個配置字典,但是這些ConfigurationProvider都派生與如下一個FileConfigurationProvider類型。FileConfigurationSource和FileConfigurationProvider都定義在“Microsoft.Extensions.Configuration
.FileExtensions”這個NuGet包中。

   1: public abstract class FileConfigurationProvider : ConfigurationProvider
   2: {
   3:  
   4:     public FileConfigurationSource Source { get; private set; }
   5:  
   6:     public FileConfigurationProvider(FileConfigurationSource source)
   7:     {
   8:         this.Source = source;
   9:         if (this.Source.ReloadOnChange)
  10:         {
  11:             ChangeToken.OnChange(
  12:                 () => this.Source.FileProvider.Watch(this.Source.Path), 
  13:                 this.Load);
  14:         }
  15:     }
  16:  
  17:     public override void Load()
  18:     {
  19:         IFileInfo fileInfo = this.Source.FileProvider.GetFileInfo(this.Source.Path);
  20:  
  21:         if ((fileInfo == null) || !fileInfo.Exists)
  22:         {
  23:             if (!this.Source.Optional)
  24:             {
  25:                 throw new FileNotFoundException();
  26:             }
  27:             base.Data = new Dictionary<string, string>(
  28:                 StringComparer.OrdinalIgnoreCase);
  29:         }
  30:         else
  31:         {
  32:             using (Stream stream = fileInfo.CreateReadStream())
  33:             {
  34:                 this.Load(stream);
  35:             }
  36:         }
  37:         base.OnReload();
  38:     }
  39:  
  40:     public abstract void Load(Stream stream);
  41: }

我們通過如上所示的代碼片段以簡化的形式模擬了FileConfigurationProvider的實現邏輯。它定義了一個抽象方法Load來完成針對配置文件的讀取和配置字典的生成,該參數代表讀取文件的輸出流。在重寫的Load方法中,它直接利用FileProvider得到描述配置文件的FileInfo對象,并調用此FileInfo對象的CreateReadStream方法得到這個Stream對象。

上面的這個代碼片段還體現了額外一些細節。首先,如果我們將FileConfigurationSource的ReloadOnChange屬性設置為True,意味著我們希望當配置文件發生該表的時候重新加載該文件。FileConfigurationProvider直接利用FileProvider的Watch方法監視配置文件的變換,并將Load方法注冊為回調從而到達配置數據同步的目的。其次,如果指定的配置文件不存在,并且FileConfigurationSource的Optional屬性被設置為True,FileConfigurationProvider是不能拋出FileNotFoundException異常的。

二、JsonConfigurationSource&JsonConfigurationProvider

JsonConfigurationSource代表針對通過JSON文件定義的配置源,該類型定義在NuGet包“Microsoft.Extensions.Configuration.Json”中。如下面的代碼片段所示,在重寫的Build方法中,如果FileProvider屬性沒有被顯式賦值,它會調用ConfigurationBuilder的擴展方法GetFileProvider得到一個FileProvdier并對該屬性賦值。Build方法最終創建并返回的是一個根據自己創建的JsonConfigurationProvider對象。作為FileConfigurationProvider的繼承者,JsonConfigurationProvider利用重寫的Load方法讀取配置文件的內容并將其轉換成配置字典。

   1: public class JsonConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new JsonConfigurationProvider(this);
   7:     }
   8: }
   9:  
  10: public class JsonConfigurationProvider : FileConfigurationProvider
  11: {
  12:     public JsonConfigurationProvider(JsonConfigurationSource source);
  13:     public override void Load(Stream stream);
  14: }

“Microsoft.Extensions.Configuration.Json”這個NuGet包為我們定義了如下所示的一系列針對IConfigurationBuilder接口的擴展方法AddJsonFile來完成針對JsonConfigurationSource的注冊。如果調用第一個AddJsonFile方法重載,我們可以利用指定的Action<JsonConfigurationSource>對象對創建的JsonConfigurationSource進行初始化。至于后續的AddJsonFile方法重載,實際上就是通過相應的參數初始化JsonConfigurationSource的Path、Optional和ReloadOnChange屬性罷了。

   1: public static class JsonConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource);
   4:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path);
   5:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional);
   6:     public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional,bool reloadOnChange);
   7: }

當使用JSON文件來定義配置的時候,我們會發現不論對于何種數據結構(復雜對象、集合、數組和字典),我們都能通過JSON格式以一種簡單而自然的方式來定義它們。同樣以前面定義的Profile類型為例,我們可以利用如下所示的三個JSON文件分別定義一個完整的Profile對象、一個Profile對象的集合以及一個Key和Value類型分別為字符串和Profile的字典。

Profile類型

   1: public class Profile
   2: {
   3:     public Gender          Gender { get; set; }
   4:     public int             Age { get; set; }
   5:     public ContactInfo     ContactInfo { get; set; }
   6: }
   7:  
   8: public class ContactInfo
   9: {
  10:     public string EmailAddress { get; set; }
  11:     public string PhoneNo { get; set; }
  12: }
  13:  
  14: public enum Gender
  15: {
  16:     Male,
  17:     Female
  18: }

Profile對象:

   1: {
   2:   "profile": {
   3:     "gender"      : "Male",
   4:     "age"         : "18",
   5:     "contactInfo": {
   6:       "email"            : "foobar@outlook.com",
   7:       "phoneNo"          : "123456789"
   8:     }
   9:   }
  10: }

Profile集合或者數組

   1: {
   2:   "profiles": [
   3:     {
   4:       "gender"     : "Male",
   5:       "age"        : "18",
   6:       "contactInfo": {
   7:         "email"       : "foo@outlook.com",
   8:         "phoneNo"     : "123"
   9:       }
  10:     },
  11:     {
  12:       "gender"    : "Male",
  13:       "age"       : "25",
  14:       "contactInfo": {
  15:         "email"     : "bar@outlook.com",
  16:         "phoneNo"      : "456"
  17:       }
  18:     },
  19:     {
  20:       "gender"    : "Female",
  21:       "age"       : "40",
  22:       "contactInfo": {
  23:         "email"        : "baz@outlook.com",
  24:         "phoneNo"      : "789"
  25:       }
  26:     }
  27:   ]
  28: }

Profile字典

   1: {
   2:   "profiles": {
   3:     "foo": {
   4:       "gender"     : "Male",
   5:       "age"        : "18",
   6:       "contactInfo": {
   7:         "email"     : "foo@outlook.com",
   8:         "phoneNo"   : "123"
   9:       }
  10:     },
  11:     "bar": {
  12:       "gender"     : "Male",
  13:       "age"        : "25",
  14:       "contactInfo": {
  15:         "email"        : "bar@outlook.com",
  16:         "phoneNo"      : "456"
  17:       }
  18:     },
  19:     "baz": {
  20:       "gender": "Female",
  21:       "age"   : "40",
  22:       "contactInfo": {
  23:         "email"      : "baz@outlook.com",
  24:         "phoneNo"    : "789"
  25:       }
  26:     }
  27:   }
  28: }


三、XmlConfiguationSource & XmlConfiguationProvider

XML也是一種常用的配置定義形式,它對數據的表達能力甚至強于JSON,基于所有類型的數據結構都可以通過XML表示出來。當我們通過一個XML元素表示一個復雜對象的時候,對象的數據成員定義成當前XML元素的子元素。如果數據成員是一個簡單數據類型,我們還可以選擇將其定義成當前XML元素的屬性(Attribute)。針對一個Profile對象,我們可以采用如下兩種不同的形式來定義。

   1: <Profile>
   2:   <Gender>Male</Gender>
   3:   <Age>18</Age>
   4:   <ContactInfo>
   5:     <EmailAddress >foobar@outlook.com</Email>
   6:     <PhoneNo>123456789</PhoneNo>
   7:   </ContactInfo>
   8: </Profile>

或者

   1: <Profile Gender="Male" Age="18">
   2:   <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123456789"/>
   3: </Profile>

雖然XML對數據結構的表達能力總體要強于JSON,但是作為配置模型的數據來源卻有自己的局限性,比如它們對集合的表現形式有點不盡如人意。舉個簡單的例子,對于一個元素類型為Profile的集合,我們可以采用具有如下結構的XML來表現。

   1: <Profiles>
   2:   <Profile Gender="Male" Age="18">
   3:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   4:   </Profile>
   5:   <Profile Gender="Male" Age="25">
   6:     <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/>
   7:   </Profile>
   8:   <Profile Gender="Male" Age="40">
   9:     <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  10:   </Profile>
  11: </Profiles>

但是這段XML卻不能正確地轉換成配置字典,原因很簡單,因為字典的Key必須是唯一的,這必然要求最終構成配置樹的每個節點必須具有不同的路徑。上面這段XML很明顯不滿足這個基本的要求,因為表示一個Profile對象的三個XML元素(<Profile>...</Profile>)是“同質”的,對于由它們表示的三個Profile對象來說,分別表示性別、年齡、電子郵箱地址和電話號碼的四個葉子節點的路徑是完全一樣的,所以根據無法作為配置字典的Key。通過前面針對配置綁定的介紹我們知道,如果需要通過配置字典來表示一個Profile對象的集合,我們需要按照如下的方式為每個集合元素加上相應的索引(“foo”、“bar”和“baz”)。

   1: foo:Gender
   2: foo:Age
   3: foo:ContactInfo:EmailAddress
   4: foo:ContactInfo:PhoneNo
   5:  
   6: bar:Gender
   7: bar:Age
   8: bar:ContactInfo:EmailAddress
   9: bar:ContactInfo:PhoneNo
  10:  
  11: baz:Gender
  12: baz:Age
  13: baz:ContactInfo:EmailAddress
  14: baz:ContactInfo:PhoneNo

按照這樣的結構,如果我們需要以XML的方式來表示一個Profile對象的集合,就不得不采用如下的結構,即采用索引來命名集合元素對應的XML元素。當時這樣的定義方式從語義的角度來講是不合理的,因為同一個集合的所有元素應該是“同質”的,同質的XML元素采用不同的名稱有點說不過去。根據配置綁定的規則,這樣的結構同樣可以表示一個由三個元素組成的Dictionary<string, Profile>對象,Key分別是“Foo”、“Bar”和“Baz”。如果用這樣的XML來表示一個字典對象,語義上就完全沒有問題了。

   1: <Profiles>
   2:   <Foo Gender="Male" Age="18">
   3:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   4:   </Foo>
   5:   <Bar Gender="Male" Age="25">
   6:     <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
   7:   </Bar>
   8:   <Baz Gender="Male" Age="18">
   9:     <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  10:   </Baz>
  11: </Profiles>

針對XML文件的配置源類型為XmlConfigurationSource,該類型定義在“Microsoft.Extensions.Configuration.Xml”這個NuGet包中。如下面的代碼片段所示,XmlConfigurationSource通過重寫的Build方法創建了一個XmlConfigurationProvider對象。作為抽象類型FileConfigurationProvider的繼承者,XmlConfigurationProvider通過重寫的Load方法完成了針對XML文件的讀取和配置字典的初始化。

   1: public class XmlConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new XmlConfigurationProvider(this);
   7:     }
   8: }
   9:  
  10: public class XmlConfigurationProvider : FileConfigurationProvider
  11: {   
  12:     public XmlConfigurationProvider(XmlConfigurationSource source);   
  13:     public override void Load(Stream stream);
  14: }

JsonConfigurationSource的注冊可以通過調用針對IConfigurationBuilder的擴展方法AddJsonFile來完成。與之類似,“Microsoft.Extensions.Configuration.Xml”這個NuGet包中同樣提供了如下一系列名為AddXmlFile的擴展方法重載來根據指定的XML文件創建相應的XmlConfigurationSource并注冊到指定的ConfigurationBuilder對象上。AddXmlFile和AddJsonFile方法具有完全一樣的聲明。

   1: public static class XmlConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path);
   4:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional);
   5:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
   6:     public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
   7: }


四、IniConfigurationSource & IniConfigurationSource

“INI”是“Initialization”的縮寫,INI文件又被稱為初始化文件,它是Windows系統普遍使用的配置文件,同時也被一些Linux和Unix系統所支持。INI文件直接以鍵值對的形式定義配置項,如下所示的代碼片段體現了INI文件的基本格式。總的來說,INI文件以單純的“{Key}={Value}”的形式定義配置項,{Value}可以定義在可選的雙引號中(如果值的前后包括空白字符,必須使用雙引號,否則會被忽略)。

   1: [Section]
   2: key1=value1
   3: key2 = " value2 "
   4: ; comment
   5: # comment
   6: / comment

除了以“{Key}={Value}”的定義的原子配置項外,我們還可以采用“[{SectionName}]”的形式定義配置節對它們進行分組。中括號(“[]”)同時作為下一個的配置節開始的標志,同時也作為上一個配置結束的標志,所以采用INI文件定義的配置節并不存在層次化的結構,即沒有“子配置節”的概念。除此之外,我們可以在INI中定義相應的注釋,注釋行前置的字符可以采用“;”、“#”或者“/”。

由于INI文件自身就體現為一個數據字典,所以我們可以采用“路徑化”的Key來定義最終綁定為復雜對象、集合或者字典的配置數據。如果采用INI文件來定義一個Profile對象的基本信息,我們就可以采用如下定義形式。

   1: Gender                         = "Male"
   2: Age                            = "18"
   3: ContactInfo:EmailAddress       = "foobar@outlook.com"
   4: ContactInfo:PhoneNo            = "123456789"

由于Profile的配置信息具有兩個層次(Profile>ContactInfo),我們可以按照如下的形式將EmailAddress和PhoneNo定義在配置節“ContactInfo”中,這個INI文件和上面是完全等效的。

   1: Gender  = "Male"
   2: Age     = "18"
   3:  
   4: [ContactInfo]
   5: EmailAddress = "foobar@outlook.com"
   6: PhoneNo      =  "123456789"

針對INI文件類型的配置源類型通過如下所示的IniConfigurationSource來表示,該類型定義在“Microsoft.Extensions.Configuration.Ini”這個NuGet包中。IniConfigurationSource在重寫的Build方法中會創建的ConfigurationProvdier類型為IniConfigurationProvider。作為抽象類FileConfigurationProvider的繼承者,IniConfigurationProvider利用重寫的Load方法完成INI文件內容的讀取和配置字典的初始化。

   1: public class IniConfigurationSource : FileConfigurationSource
   2: {
   3:     public override IConfigurationProvider Build(IConfigurationBuilder builder)
   4:     {
   5:         base.FileProvider = base.FileProvider ?? builder.GetFileProvider();
   6:         return new IniConfigurationProvider(this);
   7:     }
   8:  }
   9:  
  10: public class IniConfigurationProvider : FileConfigurationProvider
  11: {
  12:     public IniConfigurationProvider(IniConfigurationSource source);
  13:     public override void Load(Stream stream);
  14: }

既然JsonConfigurationSource和XmlConfigurationSource的注冊可以通過調用IConfigurationBuilder接口的擴展方法AddJsonFile和AddXmlFile來完成,“Microsoft.Extensions.Configuration.Ini”這個NuGet包自然會也會為IniConfigurationSource定義如下所示的AddIniFile擴展方法。

   1: public static class IniConfigurationExtensions
   2: {
   3:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path);
   4:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path, bool optional);
   5:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
   6:     public static IConfigurationBuilder AddIniFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
   7: }

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

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