ASP.NET Core 6框架揭祕例項演示[08]:配置的基本程式設計模式

語言: CN / TW / HK

.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所示)。

圖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所示。

圖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配置檔案更新觸發配置的重新載入