提到“配置”二字,我想絕大部分.NET開發人員腦海中會立即浮現出兩個特殊文件的身影,那就是我們再熟悉不過的app.config和web.config,多年以來我們已經習慣了將結構化的配置定義在這兩個XML格式的文件之中。到了.NET Core的時代,很多我們習以為常的東西都發生了改變,其中就包括定義配置的方式。總的來說,新的配置系統顯得更加輕量級,並且具有更好的擴展性,其最大的特點就是支持多樣化的數據源。我們可以采用內存的變量作為配置的數據源,也可以將配置定義在持久化的文件甚至數據庫中。在對配置系統進行系統介紹之前,我們先從編程的角度來體驗一下全新的配置讀取方式。
一、配置編程模型三要素
就編程層面來講,.NET Core的配置系統由如下圖所示的三個核心對象構成。讀取出來的配置信息最終會轉換成一個IConfiguration對象供應用程序使用。IConfigurationBuilder是IConfiguration對象的構建者,而IConfigurationSource則代表配置數據最原始的來源。
在讀取配置的時候,我們根據配置的定義方式(數據源)創建相應的IConfigurationSource對象,並將其注冊到IConfigurationBuilder對象上。提供配置的最初來源可能不止一個,我們可以注冊多個相同或者不同類型的IConfigurationSource對象到同一個IConfigurationBuilder對象上。IConfigurationBuilder對象正是利用注冊的這些IConfigurationSource對象提供的數據構建出我們在程序中使用的IConfiguration對象。
這里介紹的IConfiguration、IConfigurationSource和IConfigurationBuilder接口以及其他一些基礎類型均定義在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。對這些接口的默認實現,則大多定義在“Microsoft.Extensions.Configuration”這個NuGet包中。
二、以鍵值對的形式讀取配置
雖然大部分情況下的配置從整體來說都具有結構化層次關系,但是“原子”配置項都以體現為最簡單的“鍵值對”形式,並且鍵和值通常都是字符串。接下來我們會通過一個簡單的實例來演示如何以鍵值對的形式來讀取配置。
假設我們的應用程序需要通過配置來設定日期/時間的顯示格式,為此我們將相關的配置信息定義在如下所示的這個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; } }
我們希望通過配置的形式來控制由DateTimeFormatOptions的四個屬性所體現的顯示格式,所以我們為它定義了一個構造函數。如下面的代碼片段所示,該構造函數具有一個IConfiguration接口類型的參數。鍵值對是配置的基本表現形式,所以IConfiguration對象提供了索引使我們可以根據配置項的Key得到配置項的值,下面的代碼正是以索引的方式得到對應配置信息的。
public class DateTimeFormatOptions { ... public DateTimeFormatOptions (IConfiguration config) { LongDatePattern = config["LongDatePattern"]; LongTimePattern = config["LongTimePattern"]; ShortDatePattern = config["ShortDatePattern"]; ShortTimePattern = config ["ShortTimePattern"]; } }
要創建一個體現當前配置的DateTimeFormatOptions對象,我們必須提供這個承載相關配置信息的IConfiguration對象。正如我們前面所說,IConfiguration對象是由IConfigurationBuilder對象創建的,而原始的配置信息則是通過相應的IConfigurationSource對象來提供,所以創建一個IConfiguration對象的正確編程方式是:創建一個ConfigurationBuilder(IConfigurationBuilder接口的默認實現類型)對象並為之注冊一個或者多個IConfigurationSource對象,最后利用它來創建我們需要的IConfiguration對象。
我們通過如下的程序來讀取配置並將其轉換成一個DateTimeFormatOptions對象。簡單起見,我們采用的IConfigurationSource實現類型為MemoryConfigurationSource,它直接利用一個保存在內存中的字典對象作為最初的配置來源。如下面的代碼片段所示,我們在為MemoryConfigurationSource提供的字典對象中設置了四種類型的日期/時間顯示格式。
public class Program { public static void Main() { 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對象是否與原始的配置一致,我們將它的四個屬性打印在控制台上。程序運行之后,控制台上將會產生如下所示的輸出結果。
三、 讀取結構化的配置
真實項目中涉及的配置大都具有結構化的層次結構,所以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方法根據指定的名稱(“DateTime”和“CurrencyDecimal”)獲得這兩個子配置節。
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類型體現的配置具有如圖6-3所示的樹形層次結構。在我們前面演示的實例中,我們使用一個MemoryConfigurationSource對象來提供原始的配置信息。由於承載原始配置信息的是一個元素類型為KeyValuePair<string, string>的集合,它在物理存儲上並不具有樹形化的層次結構,那么它如何能夠提供一個結構化的IConfiguration對象承載的數據呢?
解決方案其實很簡單,對於一棵完整的配置樹,具體的配置信息最終是通過葉子節點來承載的,所以MemoryConfigurationSource只需要在配置字典中保存葉子節點的數據即可。除此之外,為了描述配置樹的結構,配置字典需要將對應葉子節點在配置樹中的路徑作為Key。所以MemoryConfigurationSource可以采用下表6-1所示的配置字典對配置樹進行“扁平化”,作為Key的路徑采用冒號(“:”)作為分隔符。
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 | $ |
如下面的代碼片段所示,我們按照表6-1所示的結構創建了一個Dictionary<string, string>對象,並利用它創建出MemoryConfigurationSource對象。在利用ConfigurationBuilder得到IConfiguration對象之后,我們調用其GetSection方法得到名稱為“Format”的配置節,並利用后者創建一個FormatOptions。
public class Program { public static void Main() { 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() .Add(new MemoryConfigurationSource { InitialData = 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對象之后,為了驗證該對象與原始配置數據是否一致,我們依然將它的相關屬性打印在控制台上。這個程序運行之后會在控制台上呈現如下所示的輸出結果。
[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]:自定義配置源