1. 程式人生 > >[ASP.NET Core 3框架揭祕] 配置[2]:讀取配置資料[下篇]

[ASP.NET Core 3框架揭祕] 配置[2]:讀取配置資料[下篇]

[接上篇]提到“配置”二字,我想絕大部分.NET開發人員腦海中會立即浮現出兩個特殊檔案的身影,那就是我們再熟悉不過的app.config和web.config,多年以來我們已經習慣了將結構化的配置定義在這兩個XML格式的檔案之中。到了.NET Core的時代,很多我們習以為常的東西都發生了改變,其中就包括定義配置的方式。總的來說,新的配置系統顯得更加輕量級,並且具有更好的擴充套件性,其最大的特點就是支援多樣化的資料來源。我們可以採用記憶體的變數作為配置的資料來源,也可以將配置定義在持久化的檔案甚至資料庫中。在對配置系統進行系統介紹之前,我們先從程式設計的角度來體驗一下全新的配置讀取方式。

四、將結構化配置直接繫結為物件

在真正的專案開發過程中,我們傾向於像我們演示的例項一樣將一組相關的配置轉換成一個POCO物件,比如演示例項中的DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions物件。在前面演示的例項中,為了建立這些封裝配置的物件,我們都是採用手工讀取配置的形式。如果定義的配置項太多的話,逐條讀取配置項其實是一項非常繁瑣的工作。

如果承載配置資料的IConfiguration物件與對應的POCO型別具有相容的結構,我們利用配置的自動繫結機制可以將IConfiguration物件直接轉換成對應的POCO物件。對於我們演示的這個例項來說,如果採用自動化配置繫結來建立對應的Options物件,那麼這些型別中實現手工繫結的建構函式就不再需要了。

在刪除所有Options型別的建構函式之後,我們修改Options物件的建立方式。如下面的程式碼片段所示,在呼叫IConfigurationBuilder的Build方法創建出對應IConfiguration物件之後,我們呼叫GetSection方法得到其“format”配置節,而FormatOptions物件不用再通過呼叫建構函式來建立,而是直接呼叫該配置節的Get<T>方法,該方法完成了從IConfiguration到POCO物件之間的自動化繫結。

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 options = new ConfigurationBuilder()
            .Add(new MemoryConfigurationSource { InitialData = source })
            .Build()
            .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}");
    }
}

修改後的程式執行之後,我們同樣會得到如下圖所示的輸出結果。

五、將配置定義在檔案中

前面演示的三個例項都是採用 MemoryConfigurationSource將一個字典物件作為配置源,接下來我們演示一種更加常見的配置定義方法,那就是將原始配置的內容定義在一個JSON檔案中。我們將原本通過一個記憶體字典物件承載的配置定義在一個JSON檔案中,為此我們在專案的根目錄下建立一個名為“appsettings.json”的配置檔案,並將該檔案的“Copy to Output Directory”屬性設定為“Copy always”,其目的是促使專案在編譯的時候能夠將此檔案拷貝到輸出目錄下。我們採用如下的形式定義關於日期/時間和貨幣的格式配置。

{
    "format": {
        "dateTime": {
            "longDatePattern" : "dddd, MMMM d, yyyy",
            "longTimePattern" : "h:mm:ss tt",
            "shortDatePattern" : "M/d/yyyy",
            "shortTimePattern": "h:mm tt"
        },
        "currencyDecimal": {
            "digits": 2,
            "symbol": "$"
        }
    }
}

 

由於配置源發生了改變,原來的MemoryConfigurationSource需要替換成JsonConfigurationSource,不過我們不需要手工建立這個JsonConfigurationSource物件,只需要呼叫IConfigurationBuilder介面的擴充套件方法AddJsonFile新增指定的JSON檔案即可。執行修改後的程式,我們依然會得到如上圖所示的輸出結果。

public class Program
{
    public static void Main()
    {
       var options = new ConfigurationBuilder()
         .AddJsonFile("appsettings.json")
         .Build()
         .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}");
    }
}

六、根據環境動態載入配置檔案

真實專案開發過程中使用的配置往往決定於應用當前執行的環境,也就是說不同的執行環境(開發、測試、預發和產品等)會採用不同的配置。如果採用基於物理檔案的配置,我們可以為不同的環境提供對應的配置檔案,具體的做法是:除了提供一個“基礎配置檔案”(比如“appsettings.json”)之外,我們還需為相應的環境提供對應的“差異化”配置檔案,後者通常採用環境名稱作為副檔名(比如“appsettings.production.json”)。

以我們目前演示的這個程式為例,現有的這個配置檔案appsettings.json可以作為基礎配置檔案,如果某個環境需要採用不同的配置,我們可以將差異化的配置定義在對應的檔案中。如下圖所示,我們額外添加了兩個配置檔案(appsettings.staging.json和appsettings.production.json),從檔案命名我們不難看出它們分別對應的是預發和產品環境。

我們在JSON檔案中定義了針對日期/時間和貨幣格式的配置,假設預發環境和產品環境需要採用不同的貨幣格式,那麼我們需要將差異化的配置定義在針對環境的兩個配置檔案中就可以了。簡單起見,我們僅僅將貨幣的小數位數定義在配置檔案中。如下面的程式碼片段所示,貨幣小數位數(預設值為2)在預發和產品環境分別被設定為3和4。

appsettings.staging.json:

{
    "format": {
        "currencyDecimal": {
            "digits": 3
        }
    }
}

 

appsettings.production.json:

{
    "format": {
        "currencyDecimal": {
            "digits": 4
        }
    }
}

 

一般來說,我們會採用環境變數來決定應用的執行環境,但是為了在演示過程中能夠靈活地進行環境切換,我們採用命令列引數(比如“/env staging”)的形式來設定環境。到目前為止,針對某一環境的配置被分佈到兩個配置檔案中,那麼我們在啟動檔案的時候就應該根據當前執行環境動態地載入對應的配置檔案。如果兩個檔案涉及到同一段配置,應該首選當前環境對應的那個配置檔案。由於配置預設採用“後來居上”的原則,所以應該先載入基礎配置檔案,再載入針對環境的配置檔案。針對執行環境的判斷以及針對環境的配置載入體現在如下所示的程式碼片段中。

class Program
{
    static void Main(string[] args)
    {
        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方法將兩個配置檔案載入進來,那麼兩個檔案合併後的內容將用於構建Build方法建立的IConfiguration物件。接下來我們以命令列的形式啟動這個控制檯程式,並通過命令列引數指定相應的環境名稱。從如圖6-6所示的輸出結果可以看出打印出來的配置資料(貨幣的小數位數)確實來源於環境對應的配置檔案。(S605)

七、配置檔案的同步

很多情況下應用程式的配置只會在啟動的時候從相應的配置源中讀取,並在整個應用的生命週期中保持不變,一旦我們需要重修更新配置,我們不得不重新啟動應用程式。.NET Core的配置模型提供了針對配置源的監控功能,它能保證一旦原始的配置改變之後應用程式能夠及時接收到通知,此時我們可以利用預先註冊的回撥進行配置的同步。

我們演示的應用程式採用JSON檔案作為配置源,所以我們希望應用程式能夠感知到該檔案的改變,並在檔案發生改變的時候自動載入新的配置比將其重新應用到程式之中。為了演示配置的同步,我們對程式做了如下的改變。

class Program
{
    static void Main()
    {
        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();
    }
}

 

表示JSON檔案配置源的JsonConfigurationSource在預設的情況下並不會監控原始檔的變化,所以我們需要在呼叫IConfigurationBuilder的擴充套件方法AddJsonFile的時候,通過傳入的reloadOnChange引數開啟這個功能。通過IConfigurationBuilder的Build方法建立的IConfiguration物件具有一個返回型別為IChangeToken的GetReloadToken方法,我們正是利用它返回的IChangeToken來感知配置源的變化。一旦配置源發生變化,IConfiguration物件將自動載入新的內容,所以我們只需要通過註冊的回撥將同一個IConfiguration物件應用到程式之中就可以。

我們的程式會在感知到配置源變化後自動將新的配置內容打印出來,所以當該程式被啟動之後,我們對appsettings.json檔案所做的任何修改都會觸發應用對該檔案的重新載入。下圖所示的輸出是我們兩次修改貨幣小數位數導致的。

[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]:自定義配置源