[ASP.NET Core 3框架揭秘] 配置[2]:讀取配置數據[下篇]

  • 2019 年 12 月 11 日
  • 筆記

[接上篇]提到「配置」二字,我想絕大部分.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}nn");          });          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]:自定義配置源