ASP.NET Core 6框架揭祕例項演示[25]:配置與承載環境的應用

語言: CN / TW / HK

與服務註冊一樣,針對配置的設定同樣可以採用三種不同的程式設計模式。第一種是利用WebApplicationBuilder的Host屬性返回的IHostBuilder物件,它可以幫助我們設定面向宿主和應用的配置。IWebHostBuilder介面上面同樣提供了一系列用來對配置進行設定的方法,我們可以將這些方法應用到WebApplicationBuilder的WebHost屬性返回的IWebHostBuilder物件上。不過還是那句話,既然推薦使用Mininal API,最好還是採用最新的程式設計方式。(本篇提供的例項已經彙總到《 ASP.NET Core 6框架揭祕-例項演示版 》)

[S1513]基於環境變數的配置初始化( 原始碼

[S1514]以鍵值對形式讀取和修改配置( 原始碼

[S1515]註冊配置源(利用IWebHostBuilder)( 原始碼

[S1516]註冊配置源(Minimal API)( 原始碼

[S1517]預設的承載環境( 原始碼

[S1518]通過配置定製承載環境( 原始碼

[S1519]利用WebApplicationOptions定製承載環境( 原始碼

[S1513]基於環境變數的配置初始化

應用啟動的時候會將當前的環境變數作為配置源來建立承載最初配置資料的IConfiguration物件,但它只會選擇名稱以“ASPNETCORE_”為字首的環境變數(通過靜態型別Host的CreateDefaultBuilder方法建立的HostBuilder預設選擇的是字首為“DOTNET_”的環境變數)。在演示針對環境變數的初始化配置之前,需要先解決配置的消費問題,即如何獲取配置資料。如下面的程式碼片段所示,我們設定兩個環境變數,它們的名稱分別為"ASPNETCORE_FOO"和"ASPNETCORE_BAR"。在呼叫WebApplication的CreateBuilder方法創建出WebApplicationBuilder物件之後,我們將它的Configuration屬性提取出來。由除錯斷言可以看出這兩個環境變數被成功轉移到配置中了。代表承載應用的WebApplication構建出來後,其Configuration屬性返回的IConfiguration物件上同樣包含著相同的配置。

using System.Diagnostics;

Environment.SetEnvironmentVariable("ASPNETCORE_FOO", "123");
Environment.SetEnvironmentVariable("ASPNETCORE_BAR", "456");

var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
Debug.Assert(configuration["foo"] == "123");
Debug.Assert(configuration["bar"] == "456");

var app = builder.Build();
configuration = app.Configuration;
Debug.Assert(configuration["foo"] == "123");
Debug.Assert(configuration["bar"] == "456");

[S1514]以鍵值對形式讀取和修改配置

我們知道IConfiguration物件是以字典的結構來儲存配置資料的,我們可以利用該物件提供的索引以鍵值對的形式來讀取和修改配置。在ASP.NET Core應用中,我們可以通過呼叫定義在IWebHostBuilder介面的GetSetting方法和UseSetting方法達到相同的目的。

public interface IWebHostBuilder
{
    string GetSetting(string key);
    IWebHostBuilder UseSetting(string key, string value);
    ...
}

如下面的程式碼片段所示,我們可以通過利用WebApplicationBuilder的WebHost屬性將對應的IWebHostBuilder物件提取出來,通過呼叫其GetSetting方法將以環境變數設定的配置提取出來。通過呼叫其UseSetting方法提供的鍵值對會儲存到應用的配置中。配置最終的狀態被固定下來後轉移到了構建的WebApplication物件上。

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseSetting("foo", "abc");
builder.WebHost.UseSetting("bar", "xyz");

Debug.Assert(builder.WebHost.GetSetting("foo") == "abc");
Debug.Assert(builder.WebHost.GetSetting("bar") == "xyz");

IConfiguration configuration = builder.Configuration;
Debug.Assert(configuration["foo"] == "abc");
Debug.Assert(configuration["bar"] == "xyz");

var app = builder.Build();
configuration = app.Configuration;
Debug.Assert(configuration["foo"] == "abc");
Debug.Assert(configuration["bar"] == "xyz");

[S1515]註冊配置源(利用IWebHostBuilder)

配置系統最大的特點是可以註冊不同的配置源。針對配置源的註冊同樣可以利用三種程式設計方式來實現,第一種就是利用WebApplicationBuilder的Host屬性返回的IHostBuilder物件,並呼叫其的ConfigureHostConfiguration和ConfigureAppConfiguration方法完成針對宿主和應用的配置,其中自然包含針對配置源的註冊。IWebHostBuilder介面也提供如下這個等效的ConfigureAppConfiguration方法。如程式碼片段所示,該方法提供的引數是一個Action<WebHostBuilderContext, IConfigurationBuilder>委託,這意味著我們可以就承載上下文對配置做針對性設定。如果提供的設定與當前承載上下文無關,我們還可以呼叫另一個引數型別為Action<IConfigurationBuilder>的ConfigureAppConfiguration方法過載。

public interface IWebHostBuilder
{
    IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate);
}

public static class WebHostBuilderExtensions
{
    public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate);
}

我們可以利用WebApplicationBuilder的WebHost屬性返回對應的IWebHostBuilder物件,並採用如下的方式利用這個物件註冊配置源。

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureAppConfiguration(config => config.AddInMemoryCollection(new Dictionary<string, string>
    {
        ["foo"] = "123",
        ["bar"] = "456"
    }));
var app = builder.Build();
Debug.Assert(app.Configuration["foo"] == "123");
Debug.Assert(app.Configuration["bar"] == "456");

[S1516]註冊配置源(Minimal API)

由於WebApplicationBuilder的Configuration屬性返回的ConfigurationManager自身就是一個IConfigurationBuilder物件,所以最直接的方式就是按照如下的方式將配置源註冊到它上面,這也是我們提供的程式設計方式。值得一提的是,如果呼叫WebApplication型別的CreateBuilder或者Create方法時傳入了命令列引數,會自動新增針對命令列引數的配置源。

using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string>
{
    ["foo"] = "123",
    ["bar"] = "456"
});
var app = builder.Build();
Debug.Assert(app.Configuration["foo"] == "123");
Debug.Assert(app.Configuration["bar"] == "456");

[S1517]預設的承載環境

如下面的程式碼片段所示,派生於IHostEnvironment介面的IWebHostEnvironment介面定義了WebRootPath和WebRootFileProvider屬性,前者表示用於存放Web資原始檔根目錄的路徑,後者則返回該路徑對應的IFileProvider物件。如果我們希望外部可以採用HTTP請求的方式直接訪問某個靜態檔案(如JavaScript、CSS和圖片檔案等),只需要將它存放於WebRootPath屬性表示的目錄之下即可。當前承載環境之間反映在WebApplicationBuilder型別如下所示的Environment屬性中。代表承載應用的WebApplication型別同樣具有這樣一個屬性。

public interface IWebHostEnvironment : IHostEnvironment
{
    string 		WebRootPath { get; set; }
    IFileProvider 	WebRootFileProvider { get; set; }
}

public sealed class WebApplicationBuilder
{
    public IWebHostEnvironment Environment { get; }
    ...
}

public sealed class WebApplication
{
    public IWebHostEnvironment Environment { get; }
    ...
}

我們簡單介紹與承載環境相關的六個屬性(包含定義在IHostEnvironment介面中的四個屬性)是如何設定的。IHostEnvironment 介面的ApplicationName代表當前應用的名稱,它的預設值為入口程式集的名稱。EnvironmentName表示當前應用所處部署環境的名稱,其中開發(Development)、預發(Staging)和產品(Production)是三種典型的部署環境。根據不同的目的可以將同一個應用部署到不同的環境中,在不同環境中部署的應用往往具有不同的設定。在預設情況下,環境的名稱為“Production”。ASP.NET Core應用會將所有的內容檔案儲存在同一個目錄下,這個目錄的絕對路徑通過IWebHostEnvironment介面的ContentRootPath屬性來表示,而ContentRootFileProvider屬性則返回針對這個目錄的PhysicalFileProvider物件。部分內容檔案可以直接作為Web資源(如JavaScript、CSS和圖片等)供客戶端以HTTP請求的方式獲取,存放此種類型內容檔案的絕對目錄通過IWebHostEnvironment介面的WebRootPath屬性來表示,而針對該目錄的PhysicalFileProvider自然可以通過對應的WebRootFileProvider屬性來獲取。

在預設情況下,由ContentRootPath屬性表示的內容檔案的根目錄就是當前工作目錄。如果該目錄下存在一個名為“wwwroot”的子目錄,那麼它將用來存放Web資源,WebRootPath屬性將返回這個目錄。如果這樣的子目錄不存在,那麼WebRootPath屬性會返回Null。針對這兩個目錄的預設設定體現在如下所示的程式碼片段中。

using System.Diagnostics;
using System.Reflection;

var builder = WebApplication.CreateBuilder();
var environment = builder.Environment;

Debug.Assert(Assembly.GetEntryAssembly()?.GetName().Name == environment.ApplicationName);
var currentDirectory = Directory.GetCurrentDirectory();

Debug.Assert(Equals( environment.ContentRootPath,  currentDirectory));
Debug.Assert(Equals(environment.ContentRootPath, currentDirectory));

var wwwRoot = Path.Combine(currentDirectory, "wwwroot");
if (Directory.Exists(wwwRoot))
{
    Debug.Assert(Equals(environment.WebRootPath, wwwRoot));
}
else
{
    Debug.Assert(environment.WebRootPath == null);
}

static bool Equals(string path1, string path2) =>string.Equals(path1.Trim(Path.DirectorySeparatorChar), path2.Trim(Path.DirectorySeparatorChar),StringComparison.OrdinalIgnoreCase);

[S1518]通過配置定製承載環境

IWebHostEnvironment物件承載的與承載環境相關的屬性(ApplicationName、EnvironmentName、ContentRootPath和WebRootPath)可以通過配置的方式進行定製,對應配置項的名稱分別為“applicationName”、“environment”、“contentRoot”和“webroot”。靜態類WebHostDefaults為它們定義了對應的屬性。通過第14章“服務承載”可知,前三個配置項的名稱同樣以靜態只讀欄位的形式定義在HostDefaults型別中。

public static class WebHostDefaults
{
    public static readonly string EnvironmentKey 	= "environment";
    public static readonly string ContentRootKey 	= "contentRoot";
    public static readonly string ApplicationKey 	= "applicationName";
    public static readonly string WebRootKey    	= "webroot";;
}

public static class HostDefaults
{
    public static readonly string EnvironmentKey = "environment";
    public static readonly string ContentRootKey = "contentRoot";
    public static readonly string ApplicationKey = "applicationName";
}

由於應用初始化過程中的很多操作都與當前的承載環境有關,所以承載環境必須在啟動應用最初的環境就被確定下來,並在整個應用生命週期內都不能改變。如果我們希望採用配置的方式來控制當前應用的承載環境,相應的設定必須在WebApplicationBuilder物件建立之前執行,在之後試圖修改相關的配置都會丟擲異常。按照這個原則,我們可以採用命令列引數的方式對承載環境進行設定。

var app = WebApplication.Create(args);
var environment = app.Environment;

Console.WriteLine($"ApplicationName:{environment.ApplicationName}");
Console.WriteLine($"ContentRootPath:{environment.ContentRootPath}");
Console.WriteLine($"WebRootPath:{environment.WebRootPath}");
Console.WriteLine($"EnvironmentName:{environment.EnvironmentName}");

上面的演示程式利用命令列引數的方式控制承載環境的四個屬性。如程式碼片段所示,我們將命令列引數傳入WebApplication型別的Create方法建立了一個WebApplication物件,然後從中提取出代表承載環境的IWebHostEnvironment物件並將其攜帶資訊輸出到控制檯上。我們命令列的方式啟動該程式,並指定了與承載環境相關的四個引數。

圖1利用命令列引數定義承載環境

除了命令列引數,使用環境變數同樣能達到相同的目的,當時應用的名稱目前無法通過對應的配置進行設定。對於上面建立的這個演示程式,我們現在換一種方式啟動它。如圖2所示,在執行“dotnet run”命令啟動程式之前,我們為承載環境的四個屬性設定了對應的環境變數。從輸出的結果可以看出,除了應用名稱依然是入口程式集名稱外,承載環境的其他三個屬性與我們設定的環境變數是一致的。

圖2利用環境變數定義承載環境

[S1519]利用WebApplicationOptions定製承載環境

承載環境除了可以採用利用上面演示的兩種方式進行設定外,我們也可以使用如下這個WebApplicationOptions配置選項。如程式碼片段所示,WebApplicationOptions定義了四個屬性,分別代表命令列引數陣列、環境名稱、應用名稱和內容根目錄路徑。WebApplicationBuilder具有如下這個引數型別為WebApplicationOptions的CreateBuilder方法。

public class WebApplicationOptions
{
    public string[] 	Args { get; set; }
    public string 	EnvironmentName { get; set; }
    public string 	ApplicationName { get; set; }
    public string 	ContentRootPath { get; set; }
}

public sealed class WebApplication
{
    public static WebApplicationBuilder CreateBuilder(WebApplicationOptions options);
    ...
}

如果利用WebApplicationOptions來對應用所在的承載環境進行設定,上面演示的程式可以改寫成如下的形式。由於WebApplicationOptions並不包含WebRootPath對應的配置選項,如果程式執行後會發現承載環境的這個屬性為空。由於IWebHostEnvironment服務提供的應用名稱會被視為一個程式集名稱,針對它的設定會影響型別的載入,所以我們基本上不會設定應用的名稱。

var options = new WebApplicationOptions
{
    Args 		= args,
    ApplicationName 	= "MyApp",
    ContentRootPath 	= Path.Combine(Directory.GetCurrentDirectory(), "contents"),
    EnvironmentName 	= "staging"
};
var app = WebApplication.CreateBuilder(options).Build();
var environment = app.Environment;
Console.WriteLine($"ApplicationName:{environment.ApplicationName}");
Console.WriteLine($"ContentRootPath:{environment.ContentRootPath}");
Console.WriteLine($"WebRootPath:{environment.WebRootPath}");
Console.WriteLine($"EnvironmentName:{environment.EnvironmentName}");