.NET的配置支持多樣化的數據源,我們可以采用內存的變量、環境變量、命令行參數、以及各種格式的配置文件作為配置的數據來源。在對配置系統進行系統介紹之前,我們通過幾個簡單的實例演示一下如何將具有不同來源的配置數據構建為一個統一的配置對象,並以相同的方式讀取具體配置節的內容。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[501]以鍵值對形式讀取配置(源代碼)
[502]讀取結構化配置(源代碼)
[503]將結構化配置綁定為對象(源代碼)
[504]將配置定義在JSON文件中(源代碼)
[505]根據環境動態加載配置文件(源代碼)
[506]配置內容的實時同步(源代碼)
[501]以鍵值對形式讀取配置
“原子”配置項體現為一個鍵值對形式,並且鍵和值通常都是字符串。假設我們需要通過配置來設定日期/時間的顯示格式,我們為此定義了如下這個DateTimeFormatOptions類型,它的四個屬性體現了針對DateTime類型的四種顯示格式(分別為長日期/時間和短日期/時間)。
public class DateTimeFormatOptions { ... public string LongDatePattern { get; set; } public string LongTimePattern { get; set; } public string ShortDatePattern { get; set; } public string ShortTimePattern { get; set; } }
我們為該類型定義了一個參數類型為IConfiguration接口的構造函數,IConfiguration對象提供的索引使我們可以采用鍵值對的形式讀取每個配置節的值,下面的代碼正是以索引的方式得到對應配置並對DateTimeFormatOptions對象的四個屬性賦值。
public class DateTimeFormatOptions { ... public DateTimeFormatOptions (IConfiguration config) { LongDatePattern = config["LongDatePattern"]; LongTimePattern = config["LongTimePattern"]; ShortDatePattern = config["ShortDatePattern"]; ShortTimePattern = config["ShortTimePattern"]; } }
正如前面所述,IConfiguration對象是由IConfigurationBuilder對象構建的,而原始的配置信息則是通過相應的IConfigurationSource對象提供的,所以創建一個IConfiguration對象的正確編程方式如下:創建一個ConfigurationBuilder(IConfigurationBuilder接口的默認實現類型)對象,並為之注冊一個或者多個IConfigurationSource對象,最后利用它來創建我們需要的IConfiguration對象。簡單起見,我們采用的IConfigurationSource實現類型為MemoryConfigurationSource,它直接利用一個保存在內存中的字典對象作為最初的配置來源。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; var source = new Dictionary<string, string> { ["longDatePattern"] = "dddd, MMMM d, yyyy", ["longTimePattern"] = "h:mm:ss tt", ["shortDatePattern"] = "M/d/yyyy", ["shortTimePattern"] = "h:mm tt" }; var config = new ConfigurationBuilder() .Add(new MemoryConfigurationSource { InitialData = source }) .Build(); var options = new DateTimeFormatOptions(config); Console.WriteLine($"LongDatePattern: {options.LongDatePattern}"); Console.WriteLine($"LongTimePattern: {options.LongTimePattern}"); Console.WriteLine($"ShortDatePattern: {options.ShortDatePattern}"); Console.WriteLine($"ShortTimePattern: {options.ShortTimePattern}");
如上面的代碼片段所示,我們創建了一個ConfigurationBuilder對象,並在它上面注冊了一個基於內存字典的MemoryConfigurationSource對象。我們接下來利用ConfigurationBuilder對象的Build方法構建IConfiguration對象來創建DateTimeFormatOptions對象。為了驗證該Options對象是否與原始配置數據一致,我們將它的四個屬性打印在控制台上。程序運行之后,控制台上的輸出結果如圖1所示)。
[502]讀取結構化配置
配置大都具有結構化的層次結構,所以IConfiguration對象同樣具有這樣的結構。我們姑且將保持樹形層次化結構的配置稱為“配置樹”,一個IConfiguration對象正好是對這棵配置樹的某個節點的描述,而整棵配置樹則可以由根節點對應的IConfiguration對象來表示。下面以實例來演示如何定義並讀取具有層次結構的配置數據。我們依然沿用上一個實例的應用場景,但現在不僅需要設置日期/時間的格式,還需要設置其他數據類型的格式,如表示貨幣的Decimal類型。因此我們定義了一個CurrencyDecimalFormatOptions類,它的Digits和Symbol屬性分別表示小數位數與貨幣符號,CurrencyDecimalFormatOptions對象依然是利用IConfiguration對象創建的。
public class CurrencyDecimalFormatOptions { public int Digits { get; set; } public string Symbol { get; set; } public CurrencyDecimalFormatOptions (IConfiguration config) { Digits = int.Parse(config["Digits"]); Symbol = config["Symbol"]; } }
我們定義了如下的FormatOptions類型將兩種配置整合在一起,它的DateTime和CurrencyDecimal屬性分別表示針對日期/時間與貨幣數字的格式設置。FormatOptions依然具有一個參數類型為IConfiguration的構造函數,它的兩個屬性均在此構造函數中被初始化。值得注意的是,初始化這兩個屬性采用的是調用這個IConfiguration對象的GetSection方法提取的“子配置節”。
public class FormatOptions { public DateTimeFormatOptions DateTime { get; set; } public CurrencyDecimalFormatOptions CurrencyDecimal { get; set; } public FormatOptions (IConfiguration config) { DateTime = new DateTimeFormatOptions (config.GetSection("DateTime")); CurrencyDecimal = new CurrencyDecimalFormatOptions (config.GetSection("CurrencyDecimal")); } }
FormatOptions類型體現的配置具有圖2所示的樹形層次結構。在前面演示的實例中,我們使用MemoryConfigurationSource對象來提供原始的配置信息,承載原始配置信息的是一個元素類型為KeyValuePair<string, string>的集合,但是它在物理存儲上並不具有樹形層次結構,那么它如何提供一個結構化的IConfiguration對象承載的數據?
圖2 樹形層次結構的配置
對於一棵完整的配置樹,具體的配置信息存儲葉子節點上,所以MemoryConfigurationSource對象只需要在配置字典中保存葉子節點的數據即可。為了描述配置樹的結構,配置字典還需要將對應葉子節點在配置樹中的路徑作為Key。所以MemoryConfigurationSource可以采用表1列舉的配置字典對配置樹進行扁平化處理。
表1 配置的物理結構
Key |
Value |
Format:DateTime:LongDatePattern |
dddd, MMMM d, yyyy |
Format:DateTime:LongTimePattern |
h:mm:ss tt |
Format:DateTime:ShortDatePattern |
M/d/yyyy |
Format:DateTime:ShortTimePattern |
h:mm tt |
Format:CurrencyDecimal:Digits |
2 |
Format:CurrencyDecimal:Symbol |
$ |
下面的演示程序按照表1列舉的結構創建了一個Dictionary<string, string>對象,並將其作為參數調用IConfigurationBuilder接口的AddInMemoryCollection擴展方法,該方法會根據提供的字段對象創建對應的了MemoryConfigurationSource對象並進行注冊。在得到IConfiguration對象之后,我們調用其GetSection方法提取出“Format”配置節,並利用它將FormatOptions對象創建出來。
using App; using Microsoft.Extensions.Configuration; var source = new Dictionary<string, string> { ["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy", ["format:dateTime:longTimePattern"] = "h:mm:ss tt", ["format:dateTime:shortDatePattern"] = "M/d/yyyy", ["format:dateTime:shortTimePattern"] = "h:mm tt", ["format:currencyDecimal:digits"] = "2", ["format:currencyDecimal:symbol"] = "$", }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var options = new FormatOptions(configuration.GetSection("Format")); var dateTime = options.DateTime; var currencyDecimal = options.CurrencyDecimal; Console.WriteLine("DateTime:"); Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}"); Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}"); Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}"); Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}"); Console.WriteLine("CurrencyDecimal:"); Console.WriteLine($"\tDigits:{currencyDecimal.Digits}"); Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");
在得到利用讀取的配置創建的 FormatOptions對象之后,為了驗證該對象與原始配置數據是否一致,我們依然將它的相關屬性打印在控制台上。這個程序運行之后在控制台上呈現的輸出結果如圖3所示。
[503]將結構化配置綁定為對象
在前面的實例中,為了創建三個Options對象,我們不得不以鍵值對的方式從IConfiguration對象中讀取每個配置節的值,如果定義的配置項太多,逐條讀取配置項其實是一項非常煩瑣的工作。如果承載配置數據的IConfiguration對象與對應的Options類型具有兼容的結構,那么利用配置的自動綁定機制可以將IConfiguration對象直接轉換成對應的Options對象。配置綁定相應的API定義在“Microsoft.Extensions.Configuration.Binder”這個NuGet包中,
在添加了上述這個NuGet包引用之后,我們刪除了三個Options類型的構造函數,然后將演示程序改寫成如下的形式。如代碼片段所示,在構建出IConfiguration對象之后,我們其調用GetSection方法提取出“Format”配置節,最終的FormatOptions對象直接調用該配置節的Get<T>方法生成出來。修改后的程序運行之后,同樣會得到圖5-4所示的輸出結果。
... var options = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build() .GetSection("Format") .Get<FormatOptions>(); ...
[504]將配置定義在JSON文件中
前面演示的三個實例都是采用MemoryConfigurationSource類型的配置源,我們下來演示JSON配置文件的使用。我們在項目根目錄下創建一個名為“appsettings.json”的配置文件,並在其中定義了如下的配置。我們將該文件的“Copy to Output Directory”屬性設置為“Copy always”(如果項目采用的SDK類型為“Microsoft .NET.Sdk”,該應用在Visual Studio中運行時會將編譯輸出目錄作為當前目錄。如果項目采用的SDK類型為 “Microsoft .NET.Sdk.Web”,那么項目根目錄就是當前執行的目錄,此時不需要設置配置文件的 “Copy to Output Directory” 屬性。),其目的是為了讓該文件在編譯的時候自動復制到輸出目錄。
{ "format": { "dateTime": { "longDatePattern": "dddd, MMMM d, yyyy", "longTimePattern": "h:mm:ss tt", "shortDatePattern": "M/d/yyyy", "shortTimePattern": "h:mm tt" }, "currencyDecimal": { "digits": 2, "symbol": "$" } } }
基於JSON文件的配置源通過JsonConfigurationSource類型來表示。JsonConfigurationSource類型定義在“Microsoft.Extensions.Configuration.Json”這個NuGet包中,所以我們需要為演示程序添加該包的引用。我們不需要手動創建這個JsonConfigurationSource對象,只需要按照如下的方式調用IConfigurationBuilder接口的AddJsonFile擴展方法添加指定的JSON文件即可。執行修改后的程序,我們依然可以得到圖3所示的輸出結果。
var options = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build() .GetSection("format") .Get<FormatOptions>(); ...
[505]根據環境動態加載配置文件
配置內容往往取決於應用當前執行的環境,不同的執行環境(開發、測試、預發和產品等)會采用不同的配置。如果采用基於物理文件的配置,我們可以為不同的環境提供對應的配置文件,具體的做法如下:除了提供一個基礎配置文件(如appsettings.json),我們還需要為相應的環境提供對應的差異化配置文件,后者通常采用環境名稱作為文件擴展名(如appsettings.production.json)。以目前演示的程序為例,現有的配置文件appsettings.json可以作為基礎配置文件,如果某個環境需要采用不同的配置,需要將差異化的配置定義在環境對應的文件中。如圖4所示,我們額外添加了兩個配置文件(appsettings.staging.json和appsettings.production.json),從文件命名可以看出這兩個配置文件分別對應預發環境和產品環境。
圖4 針對執行環境的配置文件
我們在JSON文件中定義了針對日期/時間和貨幣格式的配置,假設預發環境和產品環境需要采用不同的貨幣格式,那么就需要將差異化的配置定義在針對環境的兩個配置文件中。簡單起見,我們僅僅將貨幣的小數位數定義在配置文件中。如下面的代碼片段所示,貨幣小數位數(默認值為2)在預發環境和產品環境中分別被設置為3與4。
appsettings.staging.json:
{ "format": { "currencyDecimal": { "digits": 3 } } }
appsettings.production.json:
{ "format": { "currencyDecimal": { "digits": 4 } } }
為了在演示過程中能夠靈活地進行環境切換,可以采用命令行參數(如/env staging)來設置環境。到目前為止,針對某一環境的配置被分布到兩個配置文件中,所以在啟動文件時就應該根據當前執行環境動態地加載對應的配置文件。如果兩個文件涉及同一段配置,就應該首選當前環境對應的那個配置文件。由於配置默認采用“后來居上”的原則,所以應該先加載基礎配置文件,再加載針對環境的配置文件。針對執行環境的判斷以及針對環境的配置加載體現在如下所示的代碼片段中。
using App; using Microsoft.Extensions.Configuration; var index = Array.IndexOf(args, "/env"); var environment = index > -1 ? args[index + 1] : "Development"; var options = new ConfigurationBuilder() .AddJsonFile("appsettings.json", false) .AddJsonFile($"appsettings.{environment}.json", true) .Build() .GetSection("format") .Get<FormatOptions>(); …
如上面的代碼片段所示,在利用傳入的命令行參數確定了當前執行環境之后,我們先后兩次調用IConfigurationBuilder對象的AddJsonFile方法將兩個配置文件加載進來,兩個文件合並后的內容將用於構建最終的IConfiguration對象。我們以命令行的形式啟動這個控制台程序,並通過命令行參數指定相應的環境名稱。從圖5所示的輸出結果可以看出,打印出的配置數據(貨幣的小數位數)確實來源於環境對應的配置文件。
圖5 輸出與當前環境匹配的配置
[506]配置內容的實時同步
.NET的配置模型提供了針對配置源的監控功能,它能保證一旦原始配置改變之后應用程序能夠及時接收到通知,此時我們可以利用預先注冊的回調進行配置的同步。前面演示的應用程序采用JSON文件作為配置源,我們希望應用程序能夠感知該文件的改變,並在發生改變的時候將新的配置應用到程序之中。為了演示配置的同步,我們對程序做了如下改變。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; var config = new ConfigurationBuilder() .AddJsonFile(path: "appsettings.json",optional: true,reloadOnChange: true) .Build(); ChangeToken.OnChange(() => config.GetReloadToken(), () => { var options = config.GetSection("format").Get<FormatOptions>(); var dateTime = options.DateTime; var currencyDecimal = options.CurrencyDecimal; Console.WriteLine("DateTime:"); Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}"); Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}"); Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}"); Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}"); Console.WriteLine("CurrencyDecimal:"); Console.WriteLine($"\tDigits:{currencyDecimal.Digits}"); Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}\n\n"); }); Console.Read();
如上面的代碼片段所示,我們在調用IConfigurationBuilder接口的AddJsonFile擴展方法時將reloadOnChange參數設置為True,進而開啟在文件更新的時候自動重新加載的功能。在IConfiguration對象成功構建之后,我們調用它的GetReloadToken方法並利用返回的IChangeToken對象來感知配置源的變化的。一旦配置源發生變化,IConfiguration對象將自動加載新的內容並“自我刷新”。上述程序會在感知到配置源發生變化后自動將新的配置內容打印出來。圖6中的輸出結果是兩次修改貨幣小數位數導致的。
圖6 配置文件更新觸發配置的重新加載