ASP.NET Core 所使用的 Configuration

  • 2503
  • 0
  • 2016-04-07

前面的文章曾談過了 ASP.NET Core 的 WebHost (host engine) 與 Middleware (service pipeline),這一篇文章將會介紹 ASP.NET Core 所使用的 Configuration.

[2016-04-06] 使用 ConfigurationBuilder() 時請務必加上 .SetBasePath(..your config path here..)

在我自己的理解裡,一個網站的基礎建設包含下列幾項:

1. Runtime host egine 

2. HTTP request pipleline

3. Configuration

4. Logging

5. Security (Authentication/Authorization)

雖然我並不熟悉所有的 web 開發語言,但我相信一個成熟的 web 開發語言一定會提供這些基礎建設,讓開發者透過簡單的 API 或設定檔就可以完成這些基礎建設的工作,因此讓開發者可以更專注在商業邏輯的層面上.

歷史

在之前版本 .net framework 裡 configuration 提供了類似的功能,但是說真的,我不太喜歡它,因為沒有足夠的彈性,基本上相關的設定都在放在 web.config 裡面,讓 web.config 的維護變成了另一個挑戰.所以,在以前的時代裡,其實很少用到 .net framework 內建提供的 configuration 機制.大部份的情況下都自己寫.

後來,我曾在一年多前開發一個自己私底下用的投資策略程式,當時也重新寫了 Configuration,後來我再看到 ASP.NET Core 裡面所使用的 Configuration 時,我發現真的長的好像呀.我沒有跟開發 Configuration 的同事談過任何開發上的事,而我跟他想出來的運作結構居然很像,真的是一件很湊巧的事.接下來的內容中,先來看如何使用 Confiugration,然後再來討論它的運作結構.

使用

在前面的文章中曾列出一些 Configuration 的用法,

public Startup(IHostingEnvironment env)
{
    // Set up configuration sources.
    var builder = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

    Configuration = builder.Build();
}

用法很簡單,你只要使用 ConfigurationBuild 來加入你所需要的檔案,最後再透過 Build() 來建立 IConfigurationRoot 即可.先來看看加入檔案的部份.

[2016-04-06] 使用 ConfigurationBuilder() 時請務必加上 .SetBasePath(..your config path here..) , 請參考 https://github.com/aspnet/Announcements/issues/166

以目前的設計來看一共可以加入三種類型的檔案,分別是 *.ini, *.json, 以及 *.xml.上面的程式碼是加入 Json 的例子,你可以使用 AddIniFile() 或 AddXmlFile() 來加入不同的檔案.基本上來說 Configuration 裡面所提供的是一個 Dictionary object,所以任何的資料最後將以 key/value 的形式存入到 Dictionary 裡.若你使用 ini 檔,它的長相範例如下

@"IniKey1=IniValue1
[IniKey2]
# Comments
IniKey3=IniValue2
; Comments
IniKey4=IniValue3
IniKey5:IniKey6=IniValue4
/Comments
[CommonKey1:CommonKey2]
IniKey7=IniValue5
CommonKey3:CommonKey4=IniValue6";

當你讀取 key = IniKey1 時,答案應該是 IniVlaue1,當讀取 key = IniKey2:IniKey3 時,答案應該是 IniValue2,以此類推.

若你使用 xml 檔,它的長相範例如下:

<settings XmlKey1=""XmlValue1"">
    <!-- Comments -->
    <XmlKey2 XmlKey3=""XmlValue2"">
        <!-- Comments -->
        <XmlKey4>XmlValue3</XmlKey4>
        <XmlKey5 Name=""XmlKey6"">XmlValue4</XmlKey5>
    </XmlKey2>
    <CommonKey1 Name=""CommonKey2"" XmlKey7=""XmlValue5"">
        <!-- Comments -->
        <CommonKey3 CommonKey4=""XmlValue6"" />
    </CommonKey1>
</settings>

當讀取 key = XmlKey1 時,答案應該是 XmlValue1,當讀取 key = XmlKey2:XmlKey3 時,答案應該是 XmlValue2,當讀取 key = XmlKey2:XmlKey5:XmlKey6 時,答案應該是 XmlValue4,以此類推.在這裡要提醒的是 key/value 的組合要放在 <settings> 裡面.

若使用 json 檔,它的長相範例如下:

{
  "JsonKey1": "JsonValue1",
  "Json.Key2": {
    "JsonKey3": "JsonValue2",
    "Json.Key4": "JsonValue3",
    "JsonKey5:JsonKey6": "JsonValue4"
  },
  "CommonKey1": {
    "CommonKey2": {
      "JsonKey7": "JsonValue5",
      "CommonKey3:CommonKey4": "JsonValue6"
    }
  }
}

當 key = JsonKey1 時,value = JsonValue1,當 key = Json.Key2:JsonKey3 時,value = JsonValue2,當 key = CommonKey1:CommonKey2:JsonKey7 時,value = JsonValue5,以此類推.

以上是透過檔案的方式來將設定值加入,如果你有特別格式的檔案要加入,其實也可以自己製做屬於自己的檔案,然後延伸 Configuration 的架構來實行.未來文章再來談談這部份的延伸做法.

另外,如果你需要的方式不是透過檔案的加入,而是直接透過讀取記憶體裡其他物件值的方式來加入,在 ConfigurationBuilder 裡也提供了 AddInMemoryCollection() 的方式來達成這目的.請參考下面程式碼:

static Dictionary < string, string > _memConfigContent = new Dictionary < string, string > {
 {
  "MemKey1",
  "MemValue1"
 },
 {
  "MemKey2:MemKey3",
  "MemValue2"
 },
 {
  "MemKey2:MemKey4",
  "MemValue3"
 },
 {
  "MemKey2:MemKey5:MemKey6",
  "MemValue4"
 },
 {
  "CommonKey1:CommonKey2:MemKey7",
  "MemValue5"
 },
 {
  "CommonKey1:CommonKey2:CommonKey3:CommonKey4",
  "MemValue6"
 }
};

public static void Main() {
 var configurationBuilder = new ConfigurationBuilder();
 configurationBuilder.AddInMemoryCollection(_memConfigContent);
 var config = configurationBuilder.Build();
 Console.Write(config["MemKey1"]);
}

 簡單的說,你只要將你的物件值轉成 Dictionary<string,string>,然後再用 AddInMemoryCollection() 加入即可.所以上面程式碼將會顯示 MemValue1 在畫面上.

從前面的例子你可以看到你可以一次加入多個檔案,範例如下:

var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddIniFile("sample.ini"));
configurationBuilder.AddJsonFile("sample.json");
configurationBuilder.AddXmlFile("sample.xml");
var config = configurationBuilder.Build();

由於加入多個檔案時,可能會發生 key/value 重覆的現象,因此我前面的文章曾提過後面加入的檔案內容會覆蓋掉前面加入的檔案內容,其實這樣的說法不太精確,稍後的內容會說明.但結果是一樣的,也就是後面的檔案內容會覆寫掉前面加入的檔案內容,所以如果你在 sample.ini 有一個 key = key1 , value = value1 的組合,而在 sample.xml 有一個 key = key1 , value=value2 的組合,由於 key1 是重覆的而且 sample.xml 比 sample.ini 晚加入到 ConfigurationBuilder,所以上述程式碼當你使用 config["key1"] 時,你會得到 value2.

Configuration 還有另一項我最喜歡的機制是 Reload On Change 功能.雖然這不是新東西,但實際上這的確很有用,因為我們有時不希望因為 configuration 內容的改變而重新啟動整個應用程式.它的範例程式碼如下:

var builder = new ConfigurationBuilder();
builder.AddJsonFile("sample.json");
var config = builder.Build().ReloadOnChanged("sample.json");

最後,Configuration 裡也內建了幫你把你的系統環境變數加入進來,範例程式碼如下:

var builder = new ConfigurationBuilder();
builder.AddJsonFile("sample.json");
builder.AddEnvironmentVariables();
var config = builder.Build();

背後的設計

 Configuration 所採用的 namespace 名稱是以 Microsoft.Extensions.Configuration 為開頭,並非設計在 AspNetCore 的名稱下.從這裡你就知道這個 Configuration 是可以讓廣泛的 .net 程式所使用,不局限於 ASP.NET Core.Configuration 主要運用到的 interface 都定義在 Microsoft.Extensions.Configuration.Abstractions namespace 之下,裡面比較重要的 interface 有 IConfiguration, IConfigurationRoot, IConfigurationBuilder, IConfigurationSource, IConfigurationProvider.

IConfiguration 裡定義了 取得/設定 value 的方法,而 IConfigurationRoot 則定義 Reload().所以,任何實做 IConfigurationRoot 的 class 都必須提供這兩個方法的 implementation.IConfigurationBuilder 是用來建立整個 Configuration 內容的介面定義,所以它裡面有兩個最重要的定義就是 Add() 和 Build().Add() 就是要加入 IConfigurationSource,而 Build() 的定義就是讓每個 configuration source 做 parse content 和 create dictionary 的動作.而所謂的 source 就是指資料的來源,例如前面範例看到的 json, ini, xml 等等,因此 IConfigurationSource 也定義了一個 Build() method.IConfigurationProvider 定義了對資料讀取的動作,如 TryGet(), Set(), Load() 等,所以實做 IConfigurationProvider 的 class 必須要知道是對那一個 source 做資料讀取的動作.

所以,程式在執行時,ConfigurationBuilder 要先被加入 ConfigurationSource,這些 source 在 builder 裡面用 List 資料結構保存著,然後在 ConfigurationBuilder 執行 Build() 的動作時,會呼叫每一個 source 的 Build(),這會回傳  ConfigurationProvider 物件,然後它會被加入到 ConfigurationBuilder 裡面另一個專門放 provider 的 List 結構裡.所以當你在讀取資料時,builder 就是會到 provider List 裡用 foreach 的方式對 provider 執行 TryGet(),若有值,就直接回傳出去了.

整個基本的設計就是這樣,你要建立一個 configuration builder,然後告訴這個 builder 要接收什麼樣子的 source,最後讓 configuration builder 去做 Build 的動作,也就是去呼叫每個 source 上的 build 動作,讓每個 source 依照自己的 parser 來將資料都讀進到 source 裡面自己的 Dictionary 物件,所以每一個 source 有自己的 Dictionary 物件.configuration builder 完成 Build 動作之後,最後要回傳的是 IConfigurationRoot,因此你可以透過它來取得 source 裡面 Dictionary 的資料,也可以透過它來做 Reload() 的動作.

實做 IConfigurationBuilder 的 class 裡面定義了一個 List,加入新的 source 時,該 source 就會放到 List,所以如果你加了 Json file 再加了 XML file,那個 List 裡面第一個元素就是 json file source,第二個元素是 xml file source.當在讀取值的時候是從後面的 source 開始讀,所以如果在 json file source 和 xml file source 有重覆的 key 定義時,會回傳的是 xml file source 的 value ,而不是 json file source的,這也是我前面內容說後面內容會覆寫前面內容的原因.其實,在程式碼的實作上並不是覆寫,只是會先從後面的 source 開始找值而己,先找到就先 return 了.

以下是一些 Configuration 的 class diagram,僅供參考

若你想實做一個新的 File source,  例如 Tab file,用 tab 分來隔 key/value,你就必須從 FileConfigurationSource 和 FileConfigurationProvider 下做出新的 source 和 provider,同時再實做一些相關的 extension method.以後有機會再來寫這方面的文章.

希望以上的說明能讓你對 Microsoft.Extensions.Configuration 能知道它的使用方法以及它的設計方式.