ASP.NET Core - 配置系統之配置提供程式

啊晚發表於2023-03-10

3. 配置提供程式

上面提到,透過 IConfigurationBuilder 的實現類物件,我們可以自由地往配置系統中新增不同的配置提供程式,從而獲取不同來源的配置資訊。.NET Core 中,微軟提供了以下這些內建的配置提供程式:

  • 檔案配置提供程式
  • 環境變數配置提供程式
  • 命令列配置提供程式
  • Azure應用配置提供程式
  • Azure Key Vault 配置提供程式
  • Key-per-file配置提供程式
  • 記憶體配置提供程式
  • 應用機密(機密管理器)
  • 自定義配置提供程式

這裡稍微介紹一下常用的幾個。

3.1 檔案配置提供程式

顧名思義,這個就是我們熟悉的配置載入方式,從配置檔案中載入配置資訊。配置檔案多種多樣,.NET Core框架內建支援Json、xml、ini三種格式的檔案提供程式:

  • JSON配置提供程式(JsonConfigurationProvider)
  • XML配置提供程式(XmlConfigurationProvider)
  • INI配置提供程式(IniConfigurationProvider)

以上這些配置提供程式,均繼承於抽象基類 FileConfigurationProvider,當一個提供程式中發現重複的鍵時,提供程式會引發 FormatException,所有型別的檔案提供程式都是這樣的機制。

另外,所有檔案配置提供程式都支援提供兩個配置引數:

  • optional:bool型別,指示該檔案是否是可選的。如果該引數為false,但是指定的檔案又不存在,則會報錯。
  • reloadOnChange:bool型別,指示該檔案發生更改時,是否要重新載入配置。

3.1.1 JSON配置提供程式

JSON配置提供程式被封裝在 Microsoft.Extensions.Configuration.Json Nuget包中,若透過 ConfigurationBuilder 自行構建配置系統需要先安裝該依賴包。它透過 JsonConfigurationProvider 在執行時從 Json 檔案中載入配置。

使用方式非常簡單,透過 IConfigurationBuilder 的實現類物件呼叫 AddJsonFile 擴充套件方法指定Json配置檔案的路徑即可。以下程式碼可用於控制檯程式中建立主機並設定配置系統:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        // 新增 json 配置檔案
        config.AddJsonFile("appsettings.json",true, true)
            .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");

host.Run();

appsetting.json 配置檔案中的內容如下:

{
  "Settings": {
    "Provider": "JsonProvider",
    "version": {
      "subKey1": "value",
      "subKey2": 1
    },
    "items": [ "item1", "item2", "item3" ]
  }
}

控制檯程式執行之後輸出如下:

image

這樣有一點要注意的是,對於我們手動新增的配置檔案需要設定一下檔案屬性,讓其在專案生成的時候能夠正常生成到執行目錄,確保應用可以正常獲取到該檔案:

image

3.1.2 XML配置提供程式

XML配置提供程式被封裝在 Microsoft.Extensions.Configuration.Xml Nuget包中,透過 XmlConfigurationProvider 類在執行時從 XML 檔案載入配置。

使用方式也很簡單,與 JSON 配置提供程式類似,透過 AddXmlFile 擴充套件方法指定配置檔案路徑。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        //// 新增 json 配置檔案
        //config.AddJsonFile("appsettings.json",true, true)
        //    .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);

        config.AddXmlFile("appsettings.xml", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

xml配置檔案內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<Settings>
		<Provider>XmlProvider</Provider>
		<version>
			<subKey1>value</subKey1>
			<subKey2>1</subKey2>
		</version>
		<items>item1</items>
		<items>item2</items>
		<items>item3</items>
	</Settings>
</configuration>

執行程式控制臺輸出如下:

image

這裡有一個和版本有關的點,對Xml檔案中使用同一元素名稱的重複元素,一般也就是陣列,.NET 6及之後的xml配置提供程式會自動為其編制索引,不再需要顯式指定name屬性。如果是 .NET 6 以下的版本則需要這樣了:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<Settings>
		<Provider>XmlProvider</Provider>
		<version>
			<subKey1>value</subKey1>
			<subKey2>1</subKey2>
		</version>
		<items name="itemkey1">item1</items>
		<items name="itemkey2">item2</items>
		<items name="itemkey3">item3</items>
	</Settings>
</configuration>
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:itemkey2")}");

另外xml檔案中的屬性也可用於提供值:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <key attribute="value" />
  <section>
    <key attribute="value" />
  </section>
</configuration>

獲取屬性的值可用以下配置鍵:

key:attribute
section:key:attribute

3.1.3 INI配置提供程式

INI 配置提供程式被封裝在 Microsoft.Extensions.Configuration.Ini Nuget包,透過 IniConfigurationProvider 類在執行時從 INI 檔案載入配置。使用方式如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();

        var env = context.HostingEnvironment;
        //// 新增 json 配置檔案
        //config.AddJsonFile("appsettings.json",true, true)
        //    .AddJsonFile($"appsetting.{env.EnvironmentName}.json", true, true);

        //config.AddXmlFile("appsettings.xml", true, true);

        config.AddIniFile("appsettings.ini", true, true);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

ini配置檔案內容如下:

[Settings]
Provider="IniProvider"
version:subKey1="value"
version:subKey2=1
items:0="item1"
items:1="item2"
items:3="item3"

執行應用,控制檯輸出如下:
image

3.2 環境變數配置提供程式

環境變數配置提供程式被封裝在 Microsoft.Extensions.Configuration.EnvironmentVariables, 透過 EnvironmentVariablesConfigurationProvider 在執行時從環境變數中以鍵值對的方式載入配置。

環境變數一般情況下是配置在機器上的,而不同的作業系統對環境變數的設定要求有所不同,當環境變數存在多層的時候,層級之間的分隔有些支援透過 : 號進行分隔,有些不支援,雙下劃線()是全平臺支援的,所以設定環境變數的時候要使用雙下劃線()來代替冒號(:)。

各種不同的平臺下怎麼去新增環境變數這裡就不細說了,Windows 下大家最起碼都應該知道可以透過 我的電腦 -> 屬性 -> 高階系統設定 去視覺化的新增,命令列的方式可閱讀下官方文章: ASP.NET Core 中的配置 | Microsoft Learn,Linux 平臺下可以透過 export 命令臨時新增,或者修改相應的配置檔案 ~/.bashrc 或 /etc/profile,大家仔細查一下資料就行了。

處理在機器上直接設定環境變數外,我們開發測試的過程中也可以透過 ASP.NET Core框架下的 launchSettings.json 配置檔案設定用於除錯的臨時環境變數。在應用啟動除錯時,該檔案中的環境變數會替代系統的中的環境變數。

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "ConfigurationSample": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5004",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "Custom_settings__Provider": "EnvironmentVariablesProvider",
        "Custom_settings__version__subKey1": "value",
        "Custom_settings__items__0": "item1",
        "Custom_settings__items__1": "item2",
        "Custom_settings__items__2": "item3"
      }
    }
  }
}

環境變數配置提供程式使用也很簡單,注意以下示例為了使用 launchSettings.json 中的環境變數是在 ASP.NET Core專案中測試的。

var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureAppConfiguration(builder =>
{
    builder.Sources.Clear();
    // 篩選前置為 Custom_ 的環境變數,將其載入為應用配置,其他的不載入
    builder.AddEnvironmentVariables("Custom_");
});

var app = builder.Build();

Console.WriteLine($"Settings:Provider: {app.Configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {app.Configuration.GetValue<string>("Settings:items:1")}");

app.Run();

在新增環境變數時,透過指定引數 prefix,只讀取限定字首的環境變數。不過在讀取環境變數時,會將字首刪除。如果不指定引數 prefix,那麼會讀取所有環境變數。

當建立預設通用主機(Host)時,預設就已經新增了字首為 DOTNET_ 的環境變數,如果是在 ASP.NET Core 中,配置了 Web主機時,預設新增了字首為 ASPNETCORE_ 的環境變數,而後主機載入應用配置時,再根據策略新增了其他的環境變數,如果沒有傳遞 prefix 引數則是所有環境變數。這一塊的載入機制,下面再細講。

執行應用,控制檯輸出如下:
image

除此之外,環境變數提供程式還有一些隱藏的功能點,當沒有向 AddEnvironmentVariables 傳入字首時,預設也會針對含有以下字首的環境變數進行特殊處理:

image

這個功能點比較少用到,但是大家看到這個大概都會有點疑惑,具體的形式是怎麼樣的,下面稍微測試一下

首先在 launchSettings.json 檔案中新增多一個環境變數:

"MYSQLCONNSTR_Default": "Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"

之後在應用中列印如下兩個配置:

Console.WriteLine($"ConnectionStrings:Default: { app.Configuration.GetValue<string>("ConnectionStrings:Default") }");
Console.WriteLine($"ConnectionStrings:Default_Provider: { app.Configuration.GetValue<string>("ConnectionStrings:Default_ProviderName") }");

輸出結果如下:
image

也就是說,這種形式的環境變數會被自動轉換為兩個。

3.3 命令列配置提供程式

命令列配置提供程式被封裝在 Microsoft.Extensions.Configuration.CommandLine 包中,透過CommandLineConfigurationProvider在執行時從命令列引數鍵值對中載入配置。

當我們透過 dotnet 命令啟動一個 .NET Core 應用時,我們可以在命令後面追加一些引數,這些引數將在入口檔案中被 args 變數接收到。命令列配置提供程式使用如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();
        config.AddCommandLine(args);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

之後透過命令列程式啟動應用,並傳入相應的引數:

dotnet ConfigurationSampleConsole.dll Settings:Provider=CommandLineProvider Settings:items:1=item1

image

命令列引數的設定有三種方式:

(1) 使用 = 號連線鍵值:

dotnet ConfigurationSampleConsole.dll Settings:Provider=CommandLineProvider Settings:items:0=item1 Settings:items:1=item2

(2) 使用 / 號表示鍵,值跟在鍵後面,鍵值以空格分隔

dotnet ConfigurationSampleConsole.dll /Settings:Provider CommandLineProvider /Settings:items:0 item1 /Settings:items:1 item2

(3) 使用 -- 符號表示鍵,值跟在鍵後面,鍵值以空格分隔

dotnet ConfigurationSampleConsole.dll --Settings:Provider CommandLineProvider --Settings:items:0 item1 --Settings:items:1 item2

如果值之中本來就有空格的,可以使用 "" 號包括。

dotnet ConfigurationSampleConsole.dll --Settings:Provider CommandLineProvider --Settings:items:0 item1 --Settings:items:1 "test item2"

AddCommandLine 擴充套件方法提供了過載,允許額外傳入一個引數,該引數提供一個交換對映字典,針對命令列配置引數進行key對映。例如命令列傳入鍵是 name01 ,對映後的的鍵為 project:name。這裡有一些要注意的點:

  • 交換對映key必須以-或--開頭。當使用-開頭時,命令列引數書寫時也要以-開頭,當使用--開頭時,命令列引數書寫時可以以--或/開頭。
  • 交換對映字典中的key不區分大小寫,不能包含重複 key。如不能同時出現-n和-N,但可以同時出現 -n 和 --n 。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();

        var switchMappings = new Dictionary<string, string>
        {
            ["--b1"] = "Settings:Provider",
            ["-b2"] = "Settings:items"
        };

        config.AddCommandLine(args, switchMappings);
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();

image

3.4 記憶體配置提供程式

記憶體配置提供程式就比較簡單了,它直接被包含在 Microsoft.Extensions.Configuration,透過MemoryConfigurationProvider在執行時從記憶體中的集合中載入配置。使用方式如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // 清除原有的配置提供程式
        config.Sources.Clear();

        config.AddInMemoryCollection(new Dictionary<string, string> {
            { "Settings:Provider", "InMemoryProvider" },
            { "Settings:items:1", "MemoryItem" }
        });
    })
    .Build();

var configuration = host.Services.GetService<IConfiguration>();

Console.WriteLine($"Settings:Provider: {configuration.GetValue<string>("Settings:Provider")}");
Console.WriteLine($"Settings:items[1]: {configuration.GetValue<string>("Settings:items:1")}");

host.Run();![image]

image

3.5 配置載入順序

上面介紹了一些常用的配置提供程式,這些配置提供程式都是透過擴充套件方法新增到 ConfigurationBuilder 物件中的,而從上面 ConfigurationBuilder 的原始碼可以看出,新增一個配置提供程式的時候其實應該是新增了一個對應的 IConfigurationSource 物件,而後在 ConfigurationBuilder 中被儲存到集合中。

這就可以看出,配置系統是允許同時新增多種配置提供程式,支援多來源的配置資訊同時存在的。那麼當多個配置處理程式都被新增到配置系統之中,那我們從配置系統中透過配置鍵獲取配置值的時候是怎麼進行的呢,當多個配置提供程式存在相同的配置鍵時,我們獲取到的配置值是哪個呢?

從 ConfigurationRoot 的原始碼中可以可以看到,當我們用索引器 API 讀取配置值時,是呼叫了 GetConfiguration 方法

image

而 GetConfiguration 方法中的邏輯也很簡單,只是遍歷提供程式集合嘗試從提供程式去獲取值,需要關注的是遍歷的順序。

image

這裡的邏輯是這樣子的,倒敘進行遍歷,後新增的配置處理程式先被遍歷,一旦透過key從提供程式中獲取到值就返回結果,不再繼續遍歷。所以新增配置提供程式的順序決定相同配置鍵最終的值, 當多個配置處理程式存在相同鍵時,越後新增的配置提供程式優先順序越高,從最後的一個提供程式獲取到值之後就不再從其他處理程式獲取。

3.6 預設配置來源

上面也有提到透過主機執行和管理應用,在透過主機執行的專案中,主機在啟動的時候就已經預設新增了一些配置提供程式,所以我們建立了一個 ASP.NET Core 模板專案之後就可以獲取到 appsettings.json 等配置檔案中的配置資訊。下面介紹一下預設新增的配置提供程式。

Host.CreateDefaultBuilder(String[]) 方法或者 WebApplication.CreateBuilder(args) 方法執行的時候,會按照以下順序新增應用的配置提供程式:

(1) 記憶體配置提供程式
(2) Chained 配置提供程式(新增現有的主機配置)
(3) JSON 配置提供程式 (新增 appsettings.json 配置檔案)
(4) JSON 配置提供程式 (新增 appsettings.{Environment}.json 配置檔案)
(5) 機密管理器(僅Windows)
(6) 環境變數配置提供程式 (未限定字首)
(7) 命令列配置提供程式

配置分主機配置和應用配置,主機啟動時應用仍未啟動,主機啟動過程中的配置就是主機配置。上面第一個Chained 配置提供程式就是承接過來的主機配置。而主機配置是按照以下順序載入的:

(1) 環境變數配置提供程式(以 DOTNET_ 為字首的環境變數)
(2) 命令列配置提供程式 (命令列引數)
(3) 環境變數配置提供程式(以 ASPNETCORE_ 為字首的環境變數,如果是Web主機的話)

所以最終的應用配置載入順序應該是下面這樣:

(1) 記憶體配置提供程式
(2) 環境變數配置提供程式(以 DOTNET_ 為字首的環境變數)
(3) 命令列配置提供程式 (命令列引數)
(4) 環境變數配置提供程式(以 ASPNETCORE_ 為字首的環境變數,如果是Web主機的話)
(5) JSON 配置提供程式 (新增 appsettings.json 配置檔案)
(6) JSON 配置提供程式 (新增 appsettings.{Environment}.json 配置檔案)
(7) 機密管理器(僅Windows)
(8) 環境變數配置提供程式 (未限定字首)
(9) 命令列配置提供程式 (命令列引數)

按照越後面新增的提供程式優先的方式,最終應用配置會覆蓋主機配置,並且最優先是最後新增的命令列配置提供程式,我們可以透過以下方式列印配置系統中所有的配置提供程式,進行驗證:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

var configurationRoot = (IConfigurationRoot)app.Configuration;
foreach (var provider in configurationRoot.Providers.AsEnumerable())
{
    Console.WriteLine(provider.ToString());
}

app.Run();

最終控制檯列印出來的結果如下:

最終控制檯列印出來的結果如下:image

雖然應用配置優先,會覆蓋前面的主機配置,但是有一些變數會在初始化主機生成器的時候就提前進行鎖定,並且之後不會受應用配置的影響:

這裡提到環境名稱,其實也就是軟體執行的環境,最最基本的也會分為開發環境、生產環境兩種。軟體執行環境透過環境變數來設定,普通的.NET Core 應用環境變數key為NETCORE_ENVIRONMENT,Web應用環境變數key為ASPNETCORE_ENVIRONMENT,Web應用下如果兩者同時存在,後者會覆蓋前者。軟體應用根據不同的環境會有不同的行為邏輯,例如上面講到的 appsettings.{environment}.json 根據環境而不同的配置檔案,例如之前的 入口檔案 文章中講到的 Startup 檔案根據不同環境的分離配置方式,而我們在程式碼中有時也會根據環境處理不同的邏輯,這時候我們可以注入 IHostEnvironment 服務,透過它獲取當前應用的執行環境,入口檔案中無論是 WebApplicationBuilder 物件還是 WebApplication 物件都包含該型別的屬性。

透過環境變數設定當前執行環境,其實環境變數的值只是一個字串,我們可以設定成任意值,這是執行的,.NET Core 框架下 IHostEnvironment 也能夠正常載入到相應的環境名稱,但是.NET Core 預設只提供了對 Development、Production 和 Staging 三種環境的判別,以及相應的處理邏輯和擴充套件方法,如果是其他的自定義環境則需要開發人員自行進行相應的處理了。和 .NET Core 應用環境相關的知識點大家可以看一下官方文件: 在 ASP.NET Core 中使用多個環境 | Microsoft Learn

除了上面講到的主機配置,其他還有一些主機配置,例如URLS,但這個是可以透過應用配置設定的,讀取相應的配置值時也應用從應用配置讀取。

URLS 配置Web應用啟動後的訪問地址,這個配置可以多個地方設定,其中命令列引數最優先,其他地方設定的應該被命令列引數覆蓋。但是如果透過Kestrel 終結點方式設定了Web應用的訪問地址,那Kestrel 終結點的配置將覆蓋其他所有的訪問地址的配置。

如在 appsettings.json 中新增以下配置:

"Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://localhost:9999"
      }
    }
  }

那麼以下幾種方式設定的 URLS 都會失效:

  • UseUrls
  • 命令列上的 --urls
  • 環境變數 ASPNETCORE__URLS

也就是說,就算我們用以下命令啟動應用,應用最終的訪問地址還是以Kestrel終結點配置的為準:

dotnet run --urls="https://localhost:7777"

Kestrel 配置與 URLS 配置不是一個引數,我們可以透過在命令列或者環境變數中設定 kestrel 中間點配置來覆蓋 appsettings.json 中的,這又回到配置提供程式的優先順序問題了。

set Kestrel__Endpoints__Https__Url=https://localhost:8888

dotnet run Kestrel__Endpoints__Https__Url=https://localhost:8888

在主機啟動的邏輯中Kestrel具備更高的最終優先順序,但是其實主機內部是先根據URLS建立了一個終結點,之後又替換為 Kestrel 配置的終結點的。透過應用啟動時的控制檯輸出可以看出。

image

這種情況對於單機應用沒有什麼影響,但是對於使用自動服務發現的微服務架構而言就可能有問題了,可能導致註冊服務註冊中心的終結點是第一個,而後應用終結點又被改變,導致註冊中心記錄的服務終結點和實際的不一致。

這一篇的內容比較多,但是不大好拆分,整個知識點是一個整體,透過這一章的內容相信大家能夠對 .NET Core 框架配置系統內部的工作原理有一個詳細的瞭解。



參考文章:

ASP.NET Core 中的配置 | Microsoft Learn
配置 - .NET | Microsoft Learn
理解ASP.NET Core - 配置(Configuration)



ASP.NET Core 系列:
目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 配置系統之配置新增

相關文章