提到“配置”二字,我想絕大部分.NET開發人員腦海中會立馬浮現出兩個特殊文件的身影,那就是我們再熟悉不過的app.config和web.config,多年以來我們已經習慣了將結構化的配置定義在這兩個文件之中。到了.NET Core的時代,很多我們習以為常的東西都發生了改變,其中也包括定義配置的方式。總的來說,新的配置系統顯得更加輕量級,并且具有更好的擴展性,其最大的特點就是支持多樣化的數據源。我們可以采用內存的變量作為配置的數據源,也可以直接配置定義在持久化的文件甚至數據庫中。由于很多人都不曾接觸過這個采用全新設計的配置系統,為了讓大家對它有一個大體的認識,我們先從編程的角度來體驗一下全新的配置讀取方式。這個全新的配置系統為配置的讀取定義了非常簡單的API,這些API涉及到三個核心的對象,我們不妨稱之為“配置編程模型三要素”。 [ 本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、配置編程模型三要素
二、以鍵-值對的形式讀取配置
三、讀取結構化的配置
四、將結構化配置直接綁定為對象
一、配置編程模型三要素
就編程層面來講,.NET Core的這個配置系統由如下圖所示的三個核心對象構成。讀取出來的配置信息最終會轉換成一個Configuration對象供我們的程序使用。ConfigurationBuilder是Configuration對象的構建者,而ConfigurationSource則代表配置最原始的來源。
在讀取配置的時候,我們根據配置的定義方式創建相應的ConfigurationSource,并將其注冊到創建的ConfigurationBuilder對象上。由于提供配置的最初來源可能不止一個,所以我們可以注冊多個相同或者不同類型的ConfigurationSource對象到ConfigurationBuilder上。ConfigurationBuilder這是利用注冊的這些ConfigurationSource提供的原始數據最終構建出我們在程序中使用的Configuration對象。
根據本系列文章一貫采用的命名方式,我們應該知道上面介紹的Configuration、ConfigurationSource和ConfigurationBuilder均是對一類對象的統稱,它們在API層面都通過相應的接口(IConfiguration、IConfigurationSource和IConfigurationBuilder)來表示,這些接口均義在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。如果我們的程序中只需要使用到這些接口,我們只需要添加針對這個NuGet包的依賴。至于這些接口的默認實現類型,則大多定義在“Microsoft.Extensions.Configuration”這個NuGet包中。
二、以鍵-值對的形式讀取配置
雖然在大部分情況下的配置從整體來說都具有結構化的次關系,但是“原子”配置項都以最簡單的“鍵-值對”的形式來體現,并且鍵和值通常都是字符串,接下來我們會通過一個簡單的實例來演示如何以鍵值對的形式來讀取配置。我們創建一個針對ASP.NET Core的控制臺應用,并在project.json中按照如下的方式添加針對“Microsoft.Extensions.Configuration”這個NuGet包的依賴,配置模型就實現在這個包中。
1: {
2: ...
3: "dependencies": {
4: "Microsoft.Extensions.Configuration": "1.0.0 "
5: },
6: }
假設我們的應用程序需要通過配置來設定日期/時間的顯示格式,為此我們將相關的配置信息定義在如下所示的這個DateTimeFormatOptions類,它的四個屬性體現針對DateTime對象的四種顯示格式(分別為長日期/時間和短日期/時間)。
1: public class DateTimeFormatOptions
2: {
3: public string LongDatePattern { get; set; }
4: public string LongTimePattern { get; set; }
5: public string ShortDatePattern { get; set; }
6: public string ShortTimePattern { get; set; }
7: //其他成員
8: }
我們希望通過配置的形式來控制由DateTimeFormatOptions的四個屬性體現的日期/時間顯示格式,所以我們為它定義了一個構造函數。如下面的代碼片段所示,該構造函數具有一個IConfiguration接口類型的參數,通過上面的介紹我們知道它是配置在應用程序中體現。鍵值對是配置的基本表現形式,所以Configuration對象提供了索引使我們可以根據配置項的Key得到配置項的值,下面的代碼正式調用索引的方式得到對應配置信息的。
1: public class DateTimeFormatOptions
2: {
3: //其他成員
4: public DateTimeFormatOptions (IConfiguration config)
5: {
6: this.LongDatePattern = config ["LongDatePattern"];
7: this.LongTimePattern = config ["LongTimePattern"];
8: this.ShortDatePattern = config ["ShortDatePattern"];
9: this.ShortTimePattern = config ["ShortTimePattern"];
10: }
11: }
要創建一個體現當前配置的DateTimeFormatOptions對象,我們必須向得到這個承載相關配置信息的Configuration對象。正如我們上面所說,Configuration對象是由ConfigurationBuilder創建的,而原始的配置信息則是通過相應的ConfigurationSource來提取的,所以創建一個Configuration對象的正確編程方式是先創建一個ConfigurationBuilder對象,然后為之注冊一個或者多個ConfigurationSource對象,最后利用ConfigurationBuilder來創建我們需要的Configuration對象。
我們通過如下的程序來讀取配置并將其轉換成一個DateTimeFormatOptions對象。簡單起見,我們采用一中類型為MemoryConfigurationSource的ConfigurationSource,它直接利用一個保存在內存中的字典對象作為最初的配置來源。如下面的代碼片段所示,我們在為MemoryConfigurationSource提供的字典對象中設置了四種類型的日期/時間顯示格式。
1: Dictionary<string, string> source = new Dictionary<string, string>
2: {
3: ["longDatePattern"] = "dddd, MMMM d, yyyy",
4: ["longTimePattern"] = "h:mm:ss tt",
5: ["shortDatePattern"] = "M/d/yyyy",
6: ["shortTimePattern"] = "h:mm tt"
7: };
8:
9: IConfiguration config = new ConfigurationBuilder()
10: .Add(new MemoryConfigurationSource { InitialData = source })
11: .Build();
12:
13: DateTimeFormatOptions options = new DateTimeFormatOptions(config);
14: Console.WriteLine($"LongDatePattern: {options.LongDatePattern}");
15: Console.WriteLine($"LongTimePattern: {options.LongTimePattern}");
16: Console.WriteLine($"ShortDatePattern: {options.ShortDatePattern}");
17: Console.WriteLine($"ShortTimePattern: {options.ShortTimePattern}");
我們創建了一個ConfigurationBuilder類型的對象,并將這個MemoryConfigurationSource注冊到它上面。接下來,我們直接調用ConfigurationBuilder的Build方法創建出Configuration對象,并利用后者創建了一個DateTimeFormatOptions對象。為了驗證DateTimeFormatOptions對象是否與原始的配置一致,我們將它的四個屬性打印在控制臺上。程序運行之后,控制臺上將會產生如下所示的輸出結果。
1: LongDatePattern : dddd, MMMM d, yyyy
2: LongTimePattern : h:mm:ss tt
3: ShortDatePattern: M/d/yyyy
4: ShortTimePattern: h:mm tt
三、讀取結構化的配置
真實項目中涉及的配置大都具有結構化的層次結構,所以Configuration對象同樣具有這樣的結構。結構化配置具有一個樹形層次結構,我們不妨將其稱之為“配置樹”,一個Configuration對象最終對應著這棵配置樹的某個節點,而整棵配置樹自然可以由根節點對應的Configuration對象來表示。以鍵值對體現的“原子配置項”一般對應于配置樹中不具有子節點的“葉子節點”。
接下來我們同樣以實例的方式來演示如何定義并讀取具有層次結構的配置。我們依然沿用上一節的應用場景,不過現在我們不僅僅需要設置日期/時間的格式,還需要設置其他數據類型的格式,比如表示貨幣的Decimal類型。為此我們定義了如下一個CurrencyDecimalFormatOptions類,它的屬性Digits和Symbol分別表示小數位數和貨幣符號,一個CurrencyDecimalFormatOptions對象依然是利用一個Configuration對象來創建的。
1: public class CurrencyDecimalFormatOptions
2: {
3: public int Digits { get; set; }
4: public string Symbol { get; set; }
5:
6: public CurrencyDecimalFormatOptions (IConfiguration config)
7: {
8: this.Digits = int.Parse(config["Digits"]);
9: this.Symbol = config["Symbol"];
10: }
11: }
我們定義了另一個名為FormatOptions的類型來表示針對不同數據類型的格式設置。如下面的代碼片段所示,它的兩個屬性DateTime和CurrencyDecimal分別表示針對日期/時間和貨幣數字的格式設置。FormatOptions依然具有一個參數類型為IConfiguration接口的構造函數,它的兩個屬性均在此構造函數中被初始化。值得注意的是初始化這兩個屬性采用的是當前Configuration的“子配置節”,我們通過指定配置節名稱調用GetSection方法獲得這兩個子配置節。
1: public class FormatOptions
2: {
3: public DateTimeFormatOptions DateTime { get; set; }
4: public CurrencyDecimalFormatOptions CurrencyDecimal { get; set; }
5:
6: public FormatOptions (IConfiguration config)
7: {
8: this.DateTime = new DateTimeFormatOptions (config.GetSection("DateTime"));
9: this.CurrencyDecimal = new CurrencyDecimalFormatOptions (config.GetSection("CurrencyDecimal"));
10: }
11: }
FormatOptions類型體現的配置具有如下圖所示的樹形層次化結構。在我們上面演示的實例中,我們通過以一個MemoryConfigurationSource對象來提供原始的配置信息。由于承載原始配置信息的是一個元素類型為KeyValuePair<string, string>的集合,它在物理存儲上并不具有樹形化的層次結構,那么它如何能夠最終提供一個結構化的Configuration對象呢?
解決方案其實很簡單,對于一棵完整的配置樹,具體的配置信息最終是通過葉子節點來承載的,所以MemoryConfigurationSource只需要在配置字典中保存葉子節點的數據即可。除此之外,為了描述配置樹的結構,配置字典需要將對應葉子節點在配置樹種的路徑作為Key。所以MemoryConfigurationSource可以采用下表所示的配置字典對配置數進行“扁平化”,路徑采用冒號(“:”)作為分隔符。
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>對象,并利用它創建出MemoryConfigurationSource對象。在利用ConfigurationBuildr得到表示整個配置的Configuration對象之后,我們調用其GetSection方法得到名稱為“Format”的配置節,并利用后者創建一個FormatOptions。
1: Dictionary<string, string> source = new Dictionary<string, string>
2: {
3: ["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy",
4: ["format:dateTime:longTimePattern"] = "h:mm:ss tt",
5: ["format:dateTime:shortDatePattern"] = "M/d/yyyy",
6: ["format:dateTime:shortTimePattern"] = "h:mm tt",
7:
8: ["format:currencyDecimal:digits"] = "2",
9: ["format:currencyDecimal:symbol"] = "$",
10: };
11: IConfiguration configuration = new ConfigurationBuilder()
12: .Add(new MemoryConfigurationSource { InitialData = source })
13: .Build();
14:
15: FormatOptions options = new FormatOptions(configuration.GetSection("Format"));
16: DateTimeFormatOptions dateTime = options.DateTime;
17: CurrencyDecimalFormatOptions currencyDecimal = options.CurrencyDecimal;
18:
19: Console.WriteLine("DateTime:");
20: Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");
21: Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");
22: Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");
23: Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");
24:
25: Console.WriteLine("CurrencyDecimal:");
26: Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");
27: Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");
在得到利用讀取的配置創建的 FormatOptions對象之后,為了驗證該對象與原始配置數據是否一致,我們依然將它的相關屬性打印在控制臺上。這個程序之后之后改程序會在控制臺上呈現如下所示的輸出結果。(S02)
1: DateTime:
2: LongDatePattern : dddd, MMMM d, yyyy
3: LongTimePattern : h:mm:ss tt
4: ShortDatePattern: M/d/yyyy
5: ShortTimePattern: h:mm tt
6:
7: CurrencyDecimal:
8: Digits : 2
9: Symbol : $
四、將結構化配置直接綁定為對象
在真正的項目開發過程中,我們傾向于像我們演示的兩個實例一樣通過創建相應的類型(比如演示實例中的DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions)來定義一組相關的配置選項(Option),我們將定義配置選項(Option)的這些類型稱為Option類型。在上面演示的實例中,為了創建這些封裝配置的對象,我們都是采用手工讀取配置的形式,如果定義的配置項太多的話,逐條讀取配置項其實是一項非常繁瑣的工作。
對于一個對象來說,如果我們將它的屬性視為它的子節點,一個對象同樣具有類似于Configuration對象的樹形層次結構。如果我們根據某個Option類型的結構來定義配置,或者根據配置的結構來定義這個Option類型,Option類型的屬性成員將與某個配置節具有一一對應的關系,那么原則上我們可以自動將配置信息綁定為一個具體的Option對象。
.NET Core的配置系統采用一種叫做“Options Pattern”的編程模式來支持從原始配置到Options對象之間的綁定。這種編程模式涉及的API定義在“Microsoft.Extensions.Options.ConfigurationExtensions”這個NuGet包中,所以我們需要在project.json文件中按照如下的方式添加針對性的依賴。除此之外,“Options Pattern”涉及到對DI的使用,所以我們還需要添加針對NuGet包“Microsoft.Extensions
.DependencyInjection”的依賴。
1: {
2: ...
3: "dependencies": {
4: "Microsoft.Extensions.Options.ConfigurationExtensions" : "1.0.0",
5: "Microsoft.Extensions.DependencyInjection" : "1.0.0"
6: },
7: }
借助于Options Pattern的自動綁定機制,我們無需逐條地讀取配置,所以我們可以將這個三個Options類型(DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions)的構造函數全部刪除,只保留其屬性成員。在作為程序入口的Main方法中,我們采用如下的方式創建這個表示格式設置的FormatOptions對象。
1: ...
2: FormatOptions options = new ServiceCollection()
3: .AddOptions()
4: .Configure<FormatOptions>(config.GetSection("Format"))
5: .BuildServiceProvider()
6: .GetService<IOptions<FormatOptions>>()
7: .Value;
如上面的代碼片段所示,我們創建一個ServiceCollection對象并調用擴展方法AddOptions注冊于針對Option模型的服務。接下來我們調用Configure方法將FormatOptions這個Option類型與對應的Configuration對象進行映射。我們最后利用這個ServiceCollection對象生成一個ServiceProvider,并調用其GetService方法得到一個類型為IOptions<FormatOptions>的對象,后者的Value屬性返回的就是綁定了相關配置的FormatOptions對象。
文章列表