物理文件是我們最常用到的原始配置載體,而最佳的配置文件格式主要有三種,它們分別是JSON、XML和INI,對應的配置源類型分別是JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource,它們具有如下一個相同的基類FileConfigurationSource。
一、FileConfigurationSource
FileConfigurationSource總是利用一個IFileProvider對象來讀取配置文件,我們可以利用FileProvider屬性來設置這個對象。配置文件的路徑通過Path屬性表示,一般來說這是一個針對IFileProvider對象根目錄的相對路徑。在讀取配置文件的時候,這個路徑將會作為參數調用IFileProvider對象的GetFileInfo方法得到描述配置文件的IFileInfo對象,該對象的CreateReadStream方法最終會被調用來讀取文件內容。
public abstract class FileConfigurationSource : IConfigurationSource { public IFileProvider FileProvider { get; set; } public string Path { get; set; } public bool Optional { get; set; } public int ReloadDelay { get; set; } public bool ReloadOnChange { get; set; } public Action<FileLoadExceptionContext> OnLoadException { get; set; } public abstract IConfigurationProvider Build(IConfigurationBuilder builder); public void EnsureDefaults(IConfigurationBuilder builder); public void ResolveFileProvider(); }
ResolveFileProvider方法
如果FileProvider屬性並沒有被顯式賦值,而我們指定的配置文件路徑是一個絕對路徑(比如“c:\app\appsettings.json”),那么一個針對配置文件所在目錄(“c:\app”)的PhysicalFileProvider將會自動創建出來作為FileProvider的屬性值,而Path屬性將被設置成配置文件名。如果指定的僅僅是一個相對路徑,FileProvider屬性將不會被自動初始化。這個邏輯實現在ResolveFileProvider方法中,並體現在如下的測試程序中。
class Program { static void Main() { var source = new FakeConfigurationSource { Path = @"C:\App\appsettings.json" }; Debug.Assert(source.FileProvider == null); source.ResolveFileProvider(); var fileProvider = (PhysicalFileProvider)source.FileProvider; Debug.Assert(fileProvider.Root == @"C:\App\"); Debug.Assert(source.Path == "appsettings.json"); } private class FakeConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) => throw new NotImplementedException(); } }
EnsureDefaults方法
除了ResolveFileProvider方法,FileConfigurationSource還定義了另一個名為EnsureDefaults的方法,該方法會確保FileConfigurationSource總是具有一個用於加載配置文件的IFileProvider對象。具體來說,該方法最終會調用IConfigurationBuilder接口具有如下定義的擴展方法GetFileProvider來獲取默認的IFileProvider對象。
public static class FileConfigurationExtensions { public static IFileProvider GetFileProvider(this IConfigurationBuilder builder) { if (builder.Properties.TryGetValue("FileProvider", out object provider)) { return builder.Properties["FileProvider"] as IFileProvider; } return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty); } }
從上面給出的代碼片段可以看出,這個擴展方法 實際上是將IConfigurationBuilder對象的Properties屬性表示的字典作為了存放IFileProvider對象的容器(對應的Key為“FileProvider”)。如果這個容器中存在一個IFileProvider對象,那么它將作為方法的返回值。反之,該方法會根據當前應用的基礎目錄(默認為當前應用程序域的基礎目錄,也就是當前執行的.exe文件所在的目錄)作為根目錄創建一個PhysicalFileProvider對象。
SetFileProvider和SetBasePath方法
既然默認情況下EnsureDefaults方法會從IConfigurationBuilder對象的屬性字典中提取IFileProvider對象,那么我們可以在這個屬性字典中存放一個默認的IFileProvider對象供所有注冊在它上面的FileConfigurationSource對象共享。實際上IConfigurationBuilder接口提供了如下兩個SetFileProvider和SetBasePath擴展方法實現了這個功能。
public static class FileConfigurationExtensions { public static IConfigurationBuilder SetFileProvider( this IConfigurationBuilder builder, IFileProvider fileProvider) { builder.Properties["FileProvider"] = fileProvider; return builder; } public static IConfigurationBuilder SetBasePath( this IConfigurationBuilder builder, string basePath) =>builder.SetFileProvider(new PhysicalFileProvider(basePath)); }
可缺省配置文件
FileConfigurationSource的Optional表示當前配置源是否可以缺省。如果該屬性被設置成False,即使指定的配置文件不存在也不會拋出異常。可缺省的配置文件在支持多環境的場景中具有廣泛的應用。正如前面實例演示的一樣,我們可以按照如下的方式加載兩個配置文件,基礎配置文件appsettings.json一般包含相對全面的配置,針對某個環境的差異化配置則定義在appsettings.{environment}.json文件中。前者是必需的,后者則是可以缺省的,這保證了應用程序在缺少基於當前環境的差異化配置文件的情況下依然可以使用定義在基礎配置文件中的默認配置。
var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(path: "appsettings.json", optional: false) .AddJsonFile(path: $"appsettings.{environment}.json", optional: true) .Build();
配置數據的實時同步
FileConfigurationSource借助IFileProvider對象提供的文件系統監控功能實現了配置文件在更新后的自動實時加載功能,這個特性通過ReloadOnChange屬性來開啟或者關閉。默認情況下這個特性是關閉的,我們需要通過將這個屬性設置為True來顯式地開啟該特性。如果開啟了配置文件的重新加載功能,一旦配置文件發生變化,IFileProvider對象會在第一時間將通知發送給對應的FileConfigurationProvider對象,后者會調用Load方法重新加載配置文件。考慮到有可能針對配置文件的寫入此時尚未結束,FileConfigurationSource采用了 “延時加載” 的方式來解決這個問題,具體的延時通過ReloadDelay屬性來控制。該屬性的單位是毫秒,默認設置的延時為250毫秒。
異常處理
考慮到針對配置文件的加載不可能百分之百成功,所以FileConfigurationSource提供了相應的異常處理機制。具體來說,我們可以通過FileConfigurationSource對象的OnLoadException屬性注冊一個Action<FileLoadExceptionContext>類型的委托作為異常處理器。作為參數的FileLoadExceptionContext 對象代表FileConfigurationProvider在加載配置文件出錯的情況下為異常處理器提供的執行上下文。
public class FileLoadExceptionContext { public Exception Exception { get; set; } public FileConfigurationProvider Provider { get; set; } public bool Ignore { get; set; } }
如上面的代碼片段所示,我們可以從FileLoadExceptionContext上下文中獲取拋出的異常和當前FileConfigurationProvider對象。如果異常處理結束之后上下文對象的Ignore屬性被設置為True,FileConfigurationProvider對象會認為目前的異常(可能是原來拋出的異常,也可能是異常處理器設置的異常)是可以被忽略的,此時程序會繼續執行,否則異常還是會拋出來。順便強調一下,最終拋出來的是原來的異常,所以我們不可以通過修改上下文的Exception屬性來達到拋出另一個異常的目的。
就像我們可以為注冊到IConfigurationBuilder對象上的所有FileConfigurationSource注冊一個共享的IFileProvider對象一樣,我們也可以調用IConfigurationBuilder接口的SetFileLoadExceptionHandler擴展方法注冊一個共享的異常處理器,該方法依然是利用IConfiguration
Builder對象的屬性字典來存放這個作為異常處理器的委托對象。注冊的這個異常處理器通過對應的擴展方法GetFileLoadExceptionHandler來獲取。
public static class FileConfigurationExtensions { public static IConfigurationBuilder SetFileLoadExceptionHandler(this IConfigurationBuilder builder, Action<FileLoadExceptionContext> handler) { builder.Properties["FileLoadExceptionHandler"] = handler; return builder; } public static Action<FileLoadExceptionContext> GetFileLoadExceptionHandler(this IConfigurationBuilder builder) => builder.Properties.TryGetValue("FileLoadExceptionHandler", out object handler) ? handler as Action<FileLoadExceptionContext> : null; }
前面我們提到FileConfigurationSource的EnsureDefaults方法,這個方法除了在IFileProvider對象沒有被初始化的情況下調用IConfigurationBuilder的GetFileProvider擴展方法提供一個默認的IFileProvider對象之外,它還會在異常處理器沒有初始化的情況下調用上面這個GetFileLoad
ExceptionHandler擴展方法提供一個默認的異常處理器。
二、FileConfigurationProvider
對於配置系統默認提供的針對三種文件格式化的FileConfigurationSource類型來說,它們提供的IConfigurationProvider實現都派生於如下這個抽象基類FileConfigurationProvider。對於我們自定義的FileConfigurationSource,但我們也傾向於將這個抽象類作為對應IConfiguration
Provider實現類型的基類。
public abstract class FileConfigurationProvider : ConfigurationProvider { public FileConfigurationSource Source { get; } public FileConfigurationProvider(FileConfigurationSource source); public override void Load(); public abstract void Load(Stream stream); }
當我們創建一個FileConfigurationProvider對象的時候需要提供對應的FileConfigurationSource對象,它會賦值給Source屬性。如果指定的FileConfigurationSource對象開啟了配置文件更新監控和自動加載功能(其屬性OnLoadException返回True),FileConfigurationProvider對象會利用FileConfigurationSource對象提供的IFileProvider對象對配置文件實施監控,並通過注冊回調的方式在配置文件更新的時候調用Load方法重新加載配置。
由於FileConfigurationSource對象提供了IFileProvider對象,所以FileConfigurationProvider對象可以調用其CreateReadStream方法獲取讀取配置文件內容的流對象,因此我們可以利用這個Stream對象來完成配置的加載。根據基於Stream加載配置的功能體現在抽象方法Load上,所以FileConfigurationProvider對象的派生類都需要重寫這個方法。
三、JsonConfigurationSource
JsonConfigurationSource代表針對通過JSON文件的配置源,該類型定義在NuGet包“Microsoft.Extensions.Configuration.Json”中。從如下給出的定義可以看出,JsonConfigurationSource重寫的Build方法在提供對應的JsonConfigurationProvider對象之前會調用EnsureDefaults方法,這個方法確保用於讀取配置文件的IFileProvider對象和處理配置文件加載異常的處理器被初始化。JsonConfigurationProvider對象派生於抽象類FileConfigurationProvider,它利用重寫的Load方法讀取配置文件的內容並將其轉換成配置字典。
public class JsonConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonConfigurationProvider(this); } } public class JsonConfigurationProvider : FileConfigurationProvider { public JsonConfigurationProvider(JsonConfigurationSource source); public override void Load(Stream stream); }
IConfigurationBuilder接口具有如下幾個名為AddJsonFile擴展方法來注冊JsonConfigurationSource。如果調用第一個AddJsonFile方法重載,我們可以利用指定的Action<JsonConfigurationSource>對象對創建的JsonConfigurationSource進行初始化。至於其他AddJsonFile方法重載,實際上就是通過相應的參數初始化JsonConfigurationSource對象的Path、Optional和ReloadOnChange屬性罷了。
public static class JsonConfigurationExtensions { public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource); public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path); public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional); public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange); public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange); }
當使用JSON文件來定義配置的時候,我們會發現不論對於何種數據結構(復雜對象、集合、數組和字典),我們都能通過JSON格式以一種簡單而自然的方式來定義它們。同樣以前面定義的Profile類型為例,我們可以利用如下所示的三個JSON文件分別定義一個完整的Profile對象、一個Profile對象的集合以及一個Key和Value類型分別為字符串和Profile的字典。
Profile對象:
{ "profile": { "gender" : "Male", "age" : "18", "contactInfo" : { "email" : "foobar@outlook.com", "phoneNo": "123456789" } } }
Profile集合或者數組:
{ "profiles": [ { "gender": "Male", "age": "18", "contactInfo": { "email": "foo@outlook.com", "phoneNo": "123" } }, { "gender": "Male", "age": "25", "contactInfo": { "email": "bar@outlook.com", "phoneNo": "456" } }, { "gender": "Female", "age": "40", "contactInfo": { "email": "baz@outlook.com", "phoneNo": "789" } } ] }
Profile字典(Dictionary<string, Profile>):
{ "profiles": { "foo": { "gender": "Male", "age": "18", "contactInfo": { "email": "foo@outlook.com", "phoneNo": "123" } }, "bar": { "gender": "Male", "age": "25", "contactInfo": { "email": "bar@outlook.com", "phoneNo": "456" } }, "baz": { "gender": "Female", "age": "40", "contactInfo": { "email": "baz@outlook.com", "phoneNo": "789" } } } }
四、XmlConfiguationSource
XML也是一種常用的配置定義形式,它對數據的表達能力甚至強於JSON,幾乎所有類型的數據結構都可以通過XML表示出來。當我們通過一個XML元素表示一個復雜對象的時候,對象的數據成員定義成當前XML元素的子元素。如果數據成員是一個簡單數據類型,我們還可以選擇將其定義成當前XML元素的屬性(Attribute)。針對一個Profile對象,我們可以采用如下兩種不同的形式來定義。
<Profile> <Gender>Male</Gender> <Age>18</Age> <ContactInfo> <EmailAddress>foobar@outlook.com</EmailAddress> <PhoneNo>123456789</PhoneNo> </ContactInfo> </Profile>
或者
<Profile Gender="Male" Age="18"> <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123456789"/> </Profile>
雖然XML對數據結構的表達能力總體要強於JSON,但是作為配置模型的數據來源卻有自己的局限性,比如它們對集合的表現形式有點不盡如人意。舉個簡單的例子,對於一個元素類型為Profile的集合,我們可以采用具有如下結構的XML來表現。
<Profiles> <Profile Gender="Male" Age="18"> <ContactInfo EmailAddress ="foo@outlook.com" PhoneNo="123"/> </Profile> <Profile Gender="Male" Age="25"> <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/> </Profile> <Profile Gender="Male" Age="36"> <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/> </Profile> </Profiles>
但是這段XML卻不能正確地轉換成配置字典,原因很簡單,因為字典的Key必須是唯一的,這必然要求最終構成配置樹的每個節點必須具有不同的路徑。上面這段XML很明顯不滿足這個基本的要求,因為表示一個Profile對象的三個XML元素(<Profile>...</Profile>)是“同質”的,對於由它們表示的三個Profile對象來說,分別表示性別、年齡、電子郵箱地址和電話號碼的四個葉子節點的路徑是完全一樣的,所以根本無法作為配置字典的Key。通過前面針對配置綁定的介紹我們知道,如果需要通過配置字典來表示一個Profile對象的集合,我們需要按照如下的方式為每個集合元素加上相應的索引(“foo”、“bar”和“baz”)。
foo:Gender
foo:Age
foo:ContactInfo:EmailAddress
foo:ContactInfo:PhoneNo
bar:Gender
bar:Age
bar:ContactInfo:EmailAddress
bar:ContactInfo:PhoneNo
baz:Gender
baz:Age
baz:ContactInfo:EmailAddress
baz:ContactInfo:PhoneNo
按照這樣的結構,如果我們需要以XML的方式來表示一個Profile對象的集合,就不得不采用如下的結構。但是這樣的定義方式從語義的角度來講是不合理的,因為同一個集合的所有元素就應該是“同質”的,同質的XML元素采用不同的名稱有點說不過去。根據配置綁定的規則,這樣的結構同樣可以表示一個由三個元素組成的Dictionary<string, Profile>對象,Key分別是“Foo”、“Bar”和“Baz”。如果用這樣的XML來表示一個字典對象,語義上就完全沒有問題了。
<Profiles> <Foo Gender="Male" Age="18"> <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/> </Foo> <Bar Gender="Male" Age="25"> <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/> </Bar> <Baz Gender="Male" Age="18"> <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/> </Baz> </Profiles>
針對XML文件的配置源類型為XmlConfigurationSource,該類型定義在“Microsoft.Extensions.Configuration.Xml”這個NuGet包中。如下面的代碼片段所示,XmlConfigurationSource通過重寫的Build方法創建出對應的XmlConfigurationProvider對象。作為抽象類型FileConfigurationProvider的繼承者,XmlConfigurationProvider通過重寫的Load方法完成了針對XML文件的讀取和配置字典的初始化。
public class XmlConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new XmlConfigurationProvider(this); } } public class XmlConfigurationProvider : FileConfigurationProvider { public XmlConfigurationProvider(XmlConfigurationSource source); public override void Load(Stream stream); }
JsonConfigurationSource的注冊可以通過調用針對IConfigurationBuilder對象的擴展方法AddJsonFile來完成。與之類似,IConfigurationBuilder接口同樣具有如下一系列名為AddXmlFile的擴展方法,這些方法會幫助我們注冊根據指定XML文件創建的XmlConfigurationSource對象。
public static class XmlConfigurationExtensions { public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path); public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional); public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange); public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange); }
五、IniConfigurationSource
“INI”是“Initialization”的縮寫,INI文件又被稱為初始化文件,它是Windows系統普遍使用的配置文件,同時也被一些Linux和Unix系統所支持。INI文件直接以鍵值對的形式定義配置項,如下所示的代碼片段體現了INI文件的基本格式。總的來說,INI文件以單純的“{Key}={Value}”的形式定義配置項,{Value}可以定義在可選的雙引號中(如果值的前后包括空白字符,必須使用雙引號,否則會被忽略)。
[Section] key1=value1 key2 = " value2 " ; comment # comment / comment
除了以“{Key}={Value}”的形式定義的原子配置項外,我們還可以采用“[{SectionName}]”的形式定義配置節對它們進行分組。中括號(“[]”)作為下一個的配置節開始的標志和上一個配置節結束的標志,所以采用INI文件定義的配置節並不存在層次化的結構,即沒有“子配置節”的概念。除此之外,我們可以在INI中定義相應的注釋,注釋行前置的字符可以采用“;”、“#”或者“/”。
由於INI文件自身就體現為一個數據字典,所以我們可以采用“路徑化”的Key來定義最終綁定為復雜對象、集合或者字典的配置數據。如果采用INI文件來定義一個Profile對象的基本信息,我們就可以采用如下的定義形式。
Gender = "Male" Age = "18" ContactInfo:EmailAddress = "foobar@outlook.com" ContactInfo:PhoneNo = "123456789"
由於Profile的配置信息具有兩個層次(Profile>ContactInfo),我們可以按照如下的形式將EmailAddress和PhoneNo定義在配置節“ContactInfo”中,這個INI文件在語義表達上和上面是完全等效的。
Gender = "Male" Age = "18" [ContactInfo] EmailAddress = "foobar@outlook.com" PhoneNo = "123456789"
針對INI文件類型的配置源類型通過如下所示的IniConfigurationSource來表示,該類型定義在“Microsoft.Extensions.Configuration.Ini”這個NuGet包中。IniConfigurationSource重寫的Build方法創建的是一個IniConfigurationProvider對象。作為抽象類FileConfigurationProvider的繼承者,IniConfigurationProvider利用重寫的Load方法完成INI文件內容的讀取和配置字典的初始化。
public class IniConfigurationSource : FileConfigurationSource { public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new IniConfigurationProvider(this); } } public class IniConfigurationProvider : FileConfigurationProvider { public IniConfigurationProvider(IniConfigurationSource source); public override void Load(Stream stream); }
既然JsonConfigurationSource和XmlConfigurationSource的注冊可以通過調用IConfigurationBuilder接口的擴展方法AddJsonFile和AddXmlFile來完成,“Microsoft.Extensions. Configuration.Ini”這個NuGet包會也會為IniConfigurationSource定義如下所示的AddIniFile擴展方法。
public static class IniConfigurationExtensions { public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path); public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path, bool optional); public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange); public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange); }
[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]:自定義配置源