ASP.NET Core

2023-03-10 18:02:18

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" ]
  }
}

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

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

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>

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

這裡有一個和版本有關的點,對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"

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

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 引數則是所有環境變數。這一塊的載入機制,下面再細講。

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

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

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

首先在 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") }");

輸出結果如下:

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

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

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

(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();

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]

3.5 設定載入順序

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

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

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

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

這裡的邏輯是這樣子的,倒敘進行遍歷,後新增的設定處理程式先被遍歷,一旦通過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();

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

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

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

這裡提到環境名稱,其實也就是軟體執行的環境,最最基本的也會分為開發環境、生產環境兩種。軟體執行環境通過環境變數來設定,普通的.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 設定的終結點的。通過應用啟動時的控制檯輸出可以看出。

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

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



參考文章:

ASP.NET Core 中的設定 | Microsoft Learn
設定 - .NET | Microsoft Learn
理解ASP.NET Core - 設定(Configuration)



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