1. 程式人生 > >[ASP.NET Core 3框架揭祕] 配置[6]:多樣化的配置源[上篇]

[ASP.NET Core 3框架揭祕] 配置[6]:多樣化的配置源[上篇]

.NET Core採用的這個全新的配置模型的一個主要的特點就是對多種不同配置源的支援。我們可以將記憶體變數、命令列引數、環境變數和物理檔案作為原始配置資料的來源。如果採用物理檔案作為配置源,我們可以選擇不同的格式(比如XML、JSON和INI等)。如果這些預設支援的配置源形式還不能滿足你的需求,我們還可以通過註冊自定義IConfigurationSource的方式將其他形式資料作為配置來源。

一、MemoryConfigurationSource

在之前的例項演示都在使用MemoryConfigurationSource來提供原始的配置。我們知道MemoryConfigurationSource配置源採用一個字典物件(具體來說應該是一個元素型別為KeyValuePair<string, string>的集合)作為存放原始配置資料的容器。作為一個IConfigurationSource物件,它總是通過建立某個對應的IConfigurationProvider物件來完成具體的配置資料讀取工作,那麼MemoryConfigurationSource會提供一個怎樣的IConfigurationProvider呢?

public class MemoryConfigurationSource : IConfigurationSource
{
    public IEnumerable<KeyValuePair<string, string>> InitialData { get; set; }
    public IConfigurationProvider Build(IConfigurationBuilder builder) => new MemoryConfigurationProvider(this);
}

上面給出的程式碼片段體現了MemoryConfigurationSource的完整定義,我們可以看到它具有一個IEnumerable<KeyValuePair<string, string>>型別的屬性InitialData來存放初始的配置資料。從Build方法的實現可以看出,真正被它用來讀取原始配置資料的是一個MemoryConfigurationProvider型別的物件,該型別的定義如下面的程式碼片段所示。

public class MemoryConfigurationProvider : ConfigurationProvider,  IEnumerable<KeyValuePair<string, string>>
{
    public MemoryConfigurationProvider(MemoryConfigurationSource source);
    public void Add(string key, string value);
    public IEnumerator<KeyValuePair<string, string>> GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator();
}

從上面的程式碼片段可以看出,MemoryConfigurationProvider派生於抽象類ConfigurationProvider,同時還實現了IEnumerable<KeyValuePair<string, string>>介面。我們知道ConfigurationProvider物件直接使用一個Dictionary<string, string>來儲存配置資料,當我們根據一個MemoryConfigurationSource物件呼叫建構函式建立MemoryConfigurationProvider的時候,它只需要將通過InitialData屬性儲存的配置資料轉移到這個字典中即可。MemoryConfigurationProvider還定義了一個Add方法使我們可以在任何時候都可以向配置字典中新增一個新的配置項。

通過前面對配置模型的介紹,我們知道IConfigurationProvider物件在配置模型中所起的作用就是讀取原始的配置資料並將其轉換成配置字典。在所有的預定義的IConfigurationProvider實現型別中,MemoryConfigurationProvider最為簡單直接,因為它對應的配置源就是一個配置字典,所以根本不需要作任何的結構轉換。

在利用MemoryConfigurationSource生成配置的時候,我們需要將它註冊到IConfigurationBuilder物件之上。具體來說,我們可以像前面演示的例項一樣直接呼叫IConfigurationBuilder介面的Add方法,也可以呼叫如下所示的兩個過載的AddInMemoryCollection擴充套件方法。

public static class MemoryConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddInMemoryCollection(  this IConfigurationBuilder configurationBuilder);
    public static IConfigurationBuilder AddInMemoryCollection(  this IConfigurationBuilder configurationBuilder,   IEnumerable<KeyValuePair<string, string>> initialData);
}

二、EnvironmentVariablesConfigurationSource

顧名思義,環境變數就是描述當前執行環境並影響程序執行行為的變數。按照作用域的不同,我們將環境變數劃分成三類,即分別針對當前系統、當前使用者和當前程序的環境變數。對於Windows系統來說,系統和使用者級別的環境變數儲存在登錄檔中,其路徑分別為“HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Environment”和“HKEY_CURRENT_USER\Environment ”。

環境變數的提取和維護可以通過靜態型別Environment來完成。具體來說,我們可以呼叫它的靜態方法GetEnvironmentVariable獲得某個指定名稱的環境變數的值,而GetEnvironmentVariables方法則會返回所有的環境變數,EnvironmentVariableTarget列舉型別的引數代表環境變數作用域決定的儲存位置。如果在呼叫GetEnvironmentVariable或者GetEnvironmentVariables方法時沒有顯式指定target引數或者將引數指定為EnvironmentVariableTarget.Process,在程序初始化前存在的所有環境變數(包括針對系統、當前使用者和當前程序)將會作為候選列表。

public static class Environment
{
    public static string GetEnvironmentVariable(string variable);
    public static string GetEnvironmentVariable(string variable,  EnvironmentVariableTarget target);

    public static IDictionary GetEnvironmentVariables();
    public static IDictionary GetEnvironmentVariables( EnvironmentVariableTarget target);

    public static void SetEnvironmentVariable(string variable, string value);
    public static void SetEnvironmentVariable(string variable, string value,  EnvironmentVariableTarget target);
}

public enum EnvironmentVariableTarget
{
      Process,
      User,
      Machine
}

環境變數的新增、修改和刪除均由SetEnvironmentVariable方法來完成,如果沒有顯式指定target引數,預設採用的是EnvironmentVariableTarget.Process。如果希望刪除指定名稱的環境變數,我們只需要在呼叫這個方法的時候將value引數設定為Null或者空字串即可。

除了在程式中利用靜態型別Environment,我們還可以採用命令列的方式檢視和設定環境變數。除此之外,我們在開發環境中還可以利用“系統屬性(System Properties)”設定工具以視覺化的方式檢視和設定系統和使用者級別的環境變數(“This PC”>“Properties”>“Change Settings”>“Advanced”>“Environment Variables”)。如果採用Visual Studio 來除錯我們編寫的應用,我們可以採用設定專案屬性的方式來設定程序級別的環境變數(“Properties” > “Debug”> “Environment Variables” )。在第1章 “全新的開發體驗” 中我們提到過,設定的環境變數會被儲存到launchSettings.json檔案中。

針對環境變數的配置源通過如下這個 EnvironmentVariablesConfigurationSource型別來表示,該型別定義在NuGet包“Microsoft.Extensions.Configuration.EnvironmentVariables”之中。該型別定義了一個字串型別的屬性Prefix,它表示環境變數名的字首。如果我們設定了這個Prefix屬性,系統只會選擇名稱以此作為字首的環境變數。

public class EnvironmentVariablesConfigurationSource : IConfigurationSource
{
    public string Prefix { get; set; }
    public IConfigurationProvider Build(IConfigurationBuilder builder)=> new EnvironmentVariablesConfigurationProvider(Prefix);
}

通過前面給出的程式碼片段我們可以看出EnvironmentVariablesConfigurationSource配置源會利用對應的EnvironmentVariablesConfigurationProvider物件來讀取環境變數,此操作體現在如下所示的Load方法中。由於環境變數本身就是一個數據字典,所以EnvironmentVariables
ConfigurationProvider物件無需再進行結構上的轉換。當Load方法被執行之後,它只需要將符合條件的環境變數篩選出來並新增到自己的配置字典中即可。

public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
{
    private readonly string _prefix;
    public EnvironmentVariablesConfigurationProvider(string prefix = null) =>  _prefix = prefix ?? string.Empty;
    public override void Load()
    {
        var dictionary = Environment.GetEnvironmentVariables()
            .Cast<DictionaryEntry>()
            .Where(it => it.Key.ToString().StartsWith( _prefix, StringComparison.OrdinalIgnoreCase))
            .ToDictionary(it => it.Key.ToString().Substring(_prefix.Length),   it => it.Value.ToString());
        Data = new Dictionary<string, string>( dictionary, StringComparer.OrdinalIgnoreCase);
    }
}

值得一提的是,如果我們在建立EnvironmentVariablesConfigurationProvider物件時指定了用於篩選環境變數的字首,當符合條件的環境變數被新增到自身的配置字典之後,配置項的名稱會將此字首剔除。比如字首設定為“FOO_”,環境變數“FOO_BAR”被新增到配置字典之後,配置項 名稱會變成“BAR”,這個細節也體現在上面定義的Load方法中。

在使用EnvironmentVariablesConfigurationSource的時候,我們可以呼叫Add方法將它註冊到指定的IConfigurationBuilder物件上。除此之外,EnvironmentVariablesConfigurationSource的註冊還可以直接呼叫IConfigurationBuilder介面的如下三個過載的擴充套件方法AddEnvironmentVariables來完成。

public static class EnvironmentVariablesExtensions
{
    public static IConfigurationBuilder AddEnvironmentVariables(  this IConfigurationBuilder configurationBuilder);
    public static IConfigurationBuilder AddEnvironmentVariables(  this IConfigurationBuilder builder,  Action<EnvironmentVariablesConfigurationSource> configureSource);
    public static IConfigurationBuilder AddEnvironmentVariables(  this IConfigurationBuilder configurationBuilder, string prefix);
}

我們照例編寫一個簡單的例項來演示如何利用環境變數作為配置源。如下面的程式碼片段所示,我們呼叫Environment的靜態方法SetEnvironmentVariable方法設定了四個環境變數,變數名稱具有相同的字首TEST_。我們呼叫方法AddEnvironmentVariables建立一個Environment
VariablesConfigurationSource物件並將其註冊到建立的ConfigurationBuilder 之上,在呼叫該方法時我們將環境變數名稱字首 設定為 “TEST_”。我們最終將由ConfigurationBuilder構建出的IConfiguration物件繫結成一個Profile物件。

public class Program
{
    public static void Main()
    {
        Environment.SetEnvironmentVariable("TEST_GENDER", "Male");
        Environment.SetEnvironmentVariable("TEST_AGE", "18");
        Environment.SetEnvironmentVariable("TEST_CONTACTINFO:EMAILADDRESS", "[email protected]");
        Environment.SetEnvironmentVariable("TEST_CONTACTINFO:PHONENO", "123456789");

        var profile = new ConfigurationBuilder()
            .AddEnvironmentVariables("TEST_")
            .Build()
            .Get<Profile>();

        Debug.Assert(profile.Equals(new Profile(Gender.Male, 18, "[email protected]", "123456789")));
    }
}

三、CommandLineConfigurationSource物件

在很多情況下,我們會採用Self-Host的方式將一個ASP.NET Core應用寄宿到一個託管程序中,在這種情況下我們傾向於採用命令列的方式來啟動寄宿程式。當以命令列的形式啟動一個ASP.NET Core應用時,我們希望直接使用命名行開關(Switch)來控制應用的一些行為,所以命令列開關自然也就成為了配置常用的來源之一。配置模型針對這種配置源的支援是通過CommandLineConfigurationSource來實現的,該型別定義在NuGet包 “Microsoft.Extensions.Configuration.CommandLine”中。

在以命令列的形式執行某個命令的時候,命令列開關(包括名稱和值)體現為一個簡單的字串陣列,所以CommandLineConfigurationSource的根本目的在於將命名行開關從字串陣列轉換成配置字典。要充分理解這個轉換規則,我們先得來了解一下CommandLine
ConfigurationSource支援的命令列開關究竟採用怎樣的形式來指定。我們通過一個簡單的例項來說明命令列開關的幾種指定方式。假設我們有一個命令“exec”並採用如下所示的方式執行某個託管程式(app)。

exec app {options}

在執行這個命令的時候我們通過相應的命令列開關指定多個選項。總的來說,命令列開關的指定形式大體上分為兩種,我將它們稱為“單引數(Single Argument)”和“雙引數(Double Arguments)”。所謂單引數形式就是採用等號(“=”)將命令列開關的名稱和值通過如下方法採用一個引數來指定。

{name}={value}
{prefix}{name}={value}

對於第二種單引數命令列開關的指定形式,我們可以在開關名稱前面新增一個字首,目前的字首支援“/”、“--”和“-”三種。遵循這樣的格式,我們可以採用如下三種方式將命令列開關architecture設定為“x64”。下面的列表之所以沒有使用“-”字首,是因為這個字首要求使用“命令列開關對映(Switch Mapping)”,我們稍後會對此作單獨介紹。

exec app architecture=x64
exec app /architecture=x64
exec app --architecture=x64

除了採用單引數形式,我們還可以採用雙引數形式來指定命令列開關,所謂的“雙引數”就是使用兩個引數分別定義命令列開關的名稱和值。這種形式採用的具體格式為“{prefix}{name} {value}”,所以上述的這個命令列開關architecture也可以採用如下的方式來指定。

exec app /architecture x64
exec app –-architecture x64

命令列開關的全名和縮寫之間具有一個對映關係(Switch Mapping)。以上述的這兩個命令列開關為例,我們可以採用首字母“a”來代替“architecture”。如果使用“-”作為字首,不論採用單引數還是雙引數形式,都必須使用對映後的開關名稱。值得一提的是,同一個命令列開關可以具有多個對映,比如我們也可以同時將“architecture”對映為“arch”。假設“architecture”具有了這兩種對映,我們就可以按照如下兩種方式指定CPU架構。

exec app -a=x64
exec app -arch=x64
exec app -a x64
exec app -arch x64

在瞭解了命令列開關的指定形式之後,我們接著來說說CommandLineConfigurationSource型別和由它提供的CommandLineConfigurationProvider。由於原始的命令列引數總是體現為一個採用空格分隔的字串,這樣的字串可以進一步轉換成一個字串集合,所以CommandLineConfigurationSource物件以字串集合作為配置源。如下面的程式碼片斷所示,CommandLineConfigurationSource型別具有Args和SwitchMappings兩個屬性,前者代表承載著原始命令列引數的字串集合,後者則儲存了命令列開關的縮寫與全稱之間的對映關係。CommandLineConfigurationSource實現 的Build方法會根據這兩個屬性建立並返回一個CommandLineConfigurationProvider物件。

public class CommandLineConfigurationSource : IConfigurationSource
{
    public IEnumerable<string> Args { get; set; }
    public IDictionary<string, string> SwitchMappings { get; set; }

    public IConfigurationProvider Build(IConfigurationBuilder builder) => new CommandLineConfigurationProvider( Args,SwitchMappings); 
}

具有如下定義的CommandLineConfigurationProvider物件依然是抽象類ConfigurationProvider的繼承者。CommandLineConfigurationProvider物件的目的很明確,就是對體現為字串集合的原始命令列引數進行解析,並將解析出來的引數名稱和值新增到配置字典中 ,這一切都是在重寫的Load方法中完成的。

public class CommandLineConfigurationProvider : ConfigurationProvider
{
    protected IEnumerable<string> Args { get; }
    public CommandLineConfigurationProvider(IEnumerable<string> args,  IDictionary<string, string> switchMappings = null);
    public override void Load();
}

在採用基於命令列引數作為配置源的時候,我們可以建立一個CommandLineConfigurationSource並將其註冊到ConfigurationBuilder上。我們也可以呼叫IConfigurationBuilder介面的如下三個擴充套件方法AddCommandLine將兩個步驟合二為一。

public static class CommandLineConfigurationExtensions
{
    public static IConfigurationBuilder AddCommandLine(  this IConfigurationBuilder builder,  Action<CommandLineConfigurationSource> configureSource);
    public static IConfigurationBuilder AddCommandLine(  this IConfigurationBuilder configurationBuilder, string[] args);
    public static IConfigurationBuilder AddCommandLine(  this IConfigurationBuilder configurationBuilder, string[] args,  IDictionary<string, string> switchMappings);
}

為了讓讀者朋友們對CommandLineConfigurationSource/CommandLineConfigurationProvider解析命令列引數採用的策略有一個深刻的認識,我們來演示一個簡單的例項。如下面的程式碼片段所示,我們建立了一個ConfigurationBuilder物件並呼叫AddCommandLine方法註冊了針對命令列引數的配置源,Main方法的引數args直接作為原始的命令列引數。

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var mapping = new Dictionary<string, string>
            {
                ["-a"]     = "architecture",
                ["-arch"]     = "architecture"
            };
            var configuration = new ConfigurationBuilder()
                .AddCommandLine(args, mapping)
                .Build();
            Console.WriteLine($"Architecture: {configuration["architecture"]}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

在呼叫擴充套件方法AddCommandLine註冊CommandLineConfigurationSource的時候,我們指定了一個命令列開關對映表,它將命令列開關 “architecture” 對映為 “a” 和 “arch” 。需要注意的是,在通過字典定義命令列開關對映的時候,作為目標名稱的Key應該新增 “-” 字首。接下來我們呼叫ConfigurationBuilder的Build方法創建出IConfiguration物件,並從中提取出 “architecture” 配置項的值並打印出來。如下圖所示,我們採用命令列的形式啟動這個程式並以不同的形式指定 “architecture” 的值。

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