ASP.NET Core應用基本編程模式[4]:基於承載環境的編程

基於IHostBuilder/IHost的承載系統通過IHostEnvironment介面表示承載環境,我們利用它不僅可以得到當前部署環境的名稱,還可以獲知當前應用的名稱和存放內容文件的根目錄路徑。對於一個Web應用來說,我們需要更多的承載環境資訊,額外的資訊定義在IWebHostEnvironment介面中。[本文節選自《ASP.NET Core 3框架揭秘》第11章, 更多關於ASP.NET Core的文章請點這裡]

目錄
一、IWebHostEnvironment
二、通過配置訂製承載環境
三、針對環境的編程
     註冊服務
     註冊中間件
     配置

一、IWebHostEnvironment

如下面的程式碼片段所示,派生於IHostEnvironment介面的IWebHostEnvironment介面定義了兩個屬性:WebRootPath和WebRootFileProvider。WebRootPath屬性表示用於存放Web資源文件根目錄的路徑,WebRootFileProvider屬性則返回該路徑對應的IFileProvider對象。如果我們希望外部可以採用HTTP請求的方式直接訪問某個靜態文件(如JavaScript、CSS和圖片文件等),只需要將它存放於WebRootPath屬性表示的目錄之下即可。

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

下面簡單介紹與承載環境相關的6個屬性(包含定義在IHostEnvironment介面中的4個屬性)是如何設置的。IHostEnvironment 介面的ApplicationName代表當前應用的名稱,它的默認值取決於註冊的IStartup服務。IStartup服務旨在完成中間件的註冊,不論是調用IWebHostBuilder介面的Configure方法,還是調用它的UseStartup/UseStartup<TStartup>方法,最終都是為了註冊IStartup服務,所以這兩個方法是不能被重複調用的。如果多次調用這兩個方法,最後一次調用針對IStartup的服務註冊會覆蓋前面的註冊。

如果IStartup服務是通過調用IWebHostBuilder介面的Configure方法註冊的,那麼應用的名稱由調用該方法提供的Action<IApplicationBuilder>對象來決定。具體來說,每個委託對象都會綁定到一個方法上,而方法是定義在某個類型中的,該類型所在程式集的名稱會默認作為應用的名稱。如果通過調用IWebHostBuilder介面的UseStartup/UseStartup<TStartup>方法來註冊IStartup服務,那麼註冊的Startup類型所在的程式集名稱就是應用名稱。在默認情況下,針對應用名稱的設置體現在如下所示的程式碼片段中。

public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configure)
{
    var applicationName = configure.GetMethodInfo().DeclaringType .GetTypeInfo().Assembly.GetName().Name;
    ...
}

public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder,  Type startupType)
{
    var applicationName = startupType.GetTypeInfo().Assembly.GetName().Name;
    ...
}

EnvironmentName表示當前應用所處部署環境的名稱,其中開發(Development)、預發(Staging)和產品(Production)是3種典型的部署環境。根據不同的目的可以將同一個應用部署到不同的環境中,在不同環境中部署的應用往往具有不同的設置。在默認情況下,環境的名稱為Production。

當我們編譯發布一個ASP.NET Core項目時,項目的源程式碼文件會被編譯成二進位並打包到相應的程式集中,而另外一些文件(如JavaScript、CSS和表示View的.cshtml文件等)會複製到目標目錄中,我們將這些文件稱為內容文件(Content File)。ASP.NET Core應用會將所有的內容文件存儲在同一個目錄下,這個目錄的絕對路徑通過IWebHostEnvironment介面的ContentRootPath屬性來表示,而ContentRootFileProvider屬性則返回針對這個目錄的PhysicalFileProvider對象。部分內容文件可以直接作為Web資源(如JavaScript、CSS和圖片等)供客戶端以HTTP請求的方式獲取,存放此種類型內容文件的絕對目錄通過IWebHostEnvironment介面的WebRootPath屬性來表示,而針對該目錄的PhysicalFileProvider自然可以通過對應的WebRootFileProvider屬性來獲取。

在默認情況下,由ContentRootPath屬性表示的內容文件的根目錄就是當前應用程式域的基礎目錄,也就是表示當前應用程式域的AppDomain對象的BaseDirectory屬性返回的目錄,靜態類AppContext的BaseDirectory屬性返回的也是這個目錄。對於一個通過Visual Studio創建的 .NET Core項目來說,該目錄就是編譯後保存生成的程式集的目錄(如「\bin\Debug\netcoreapp3.0」或者「\bin\Release\netcoreapp3.0」)。如果該目錄下存在一個名為「wwwroot」的子目錄,那麼它將用來存放Web資源,WebRootPath屬性將返回這個目錄;如果這樣的子目錄不存在,那麼WebRootPath屬性會返回Null。針對這兩個目錄的默認設置體現在如下所示的程式碼片段中。

class Program
{
    static void Main()
    {       
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builderUseStartup<Startup>())
        .Build()
        .Run();
    }
}
public class Startup
{
    public Startup(IWebHostEnvironment environment)
    {
        Debug.Assert(environment.ContentRootPath == AppDomain.CurrentDomain.BaseDirectory);
        Debug.Assert(environment.ContentRootPath == AppContext.BaseDirectory);

        var wwwRoot = Path.Combine(AppContext.BaseDirectory, "wwwroot");
        if (Directory.Exists(wwwRoot))
        {
            Debug.Assert(environment.WebRootPath == wwwRoot);
        }
        else
        {
            Debug.Assert(environment.WebRootPath == null);
        }
    }
    public void Configure(IApplicationBuilder app) {}
}

二、通過配置訂製承載環境

IWebHostEnvironment對象承載的4個與承載環境相關的屬性(ApplicationName、EnvironmentName、ContentRootPath和WebRootPath)可以通過配置的方式進行訂製,對應配置項的名稱分別為applicationName、environment、contentRoot和webroot。如果記不住這些配置項的名稱也沒有關係,因為我們可以利用定義在靜態類WebHostDefaults中如下所示的4個只讀屬性來得到它們的值。通過第11章的介紹可知,前三個配置項的名稱同樣以靜態只讀欄位的形式定義在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";
}

下面演示如何通過配置的方式來設置當前的承載環境。在如下這段實常式序中,我們調用IWebHostBuilder介面的UseSetting方法針對上述4個配置項做了相應的設置。由於針對UseStartup<TStartup>方法的調用會設置應用的名稱,所以通過調用UseSetting方法針對應用名稱的設置需要放在後面才有意義。相對於當前目錄(項目根目錄)的兩個子目錄「contents」和「contents/web」是我們為ContentRootPath屬性與WebRootPath屬性設置的,由於系統會驗證設置的目錄是否存在,所以必須預先創建這兩個目錄。

class Program
{
    static void Main()
    {
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
            .ConfigureLogging(options => options.ClearProviders())
            .UseStartup<Startup>()
            .UseSetting("environment", "Staging")
            .UseSetting("contentRoot", Path.Combine(Directory.GetCurrentDirectory(), "contents"))
            .UseSetting("webroot", Path.Combine(Directory.GetCurrentDirectory(), "contents/web"))
            .UseSetting("ApplicationName", "MyApp"))
        .Build()
        .Run();
    }

    public class Startup
    {
        public Startup(IWebHostEnvironment environment)
        {
            Console.WriteLine($"ApplicationName: {environment.ApplicationName}");
            Console.WriteLine($"EnvironmentName: {environment.EnvironmentName}");
            Console.WriteLine($"ContentRootPath: {environment.ContentRootPath}"); 
            Console.WriteLine($"WebRootPath: {environment.WebRootPath}");
        }
        public void Configure(IApplicationBuilder app) { }
    }
}

我們在註冊的Startup類型的構造函數中注入了IWebHostEnvironment服務,並直接將這4個屬性輸出到控制台上。我們在目錄「C:\App」下運行這個程式後,設置的4個與承載相關的屬性會以下圖所示的形式呈現在控制台上。

14

由於IWebHostEnvironment服務提供的應用名稱會被視為一個程式集名稱,針對它的設置會影響類型的載入,所以我們基本上不會設置應用的名稱。至於其他3個屬性,除了採用最原始的方式設置相應的配置項,我們還可以直接調用IWebHostBuilder介面中如下3個對應的擴展方法來設置。通過本系列之前文章介紹可知,IHostBuilder介面也有類似的擴展方法。

public static class HostingAbstractionsWebHostBuilderExtensions
{
    public static IWebHostBuilder UseEnvironment(this IWebHostBuilder hostBuilder, string environment);
    public static IWebHostBuilder UseContentRoot(this IWebHostBuilder hostBuilder, string contentRoot);
    public static IWebHostBuilder UseWebRoot(this IWebHostBuilder hostBuilder, string webRoot);
}

public static class HostingHostBuilderExtensions
{
    public static IHostBuilder UseContentRoot(this IHostBuilder hostBuilder, string contentRoot);
    public static IHostBuilder UseEnvironment(this IHostBuilder hostBuilder,  string environment);
}

三、針對環境的編程

對於同一個ASP.NET Core應用來說,我們添加的服務註冊、提供的配置和註冊的中間件可能會因部署環境的不同而有所差異。有了這個可以隨意注入的IWebHostEnvironment服務,我們可以很方便地知道當前的部署環境並進行有針對性的差異化編程。

IHostEnvironment介面提供了如下這個名為IsEnvironment的擴展方法,用於確定當前是否為指定的部署環境。除此之外,IHostEnvironment介面還提供額外3個擴展方法來進行針對3種典型部署環境(開發、預發和產品)的判斷,這3種環境採用的名稱分別為Development、Staging和Production,對應靜態類型EnvironmentName的3個只讀欄位。

public static class HostEnvironmentEnvExtensions
{
    public static bool IsDevelopment(this IHostEnvironment hostEnvironment);
    public static bool IsProduction(this IHostEnvironment hostEnvironment);
    public static bool IsStaging(this IHostEnvironment hostEnvironment); 
    public static bool IsEnvironment(this IHostEnvironment hostEnvironment, string environmentName);
}

public static class EnvironmentName
{
    public static readonly string Development = "Development";
    public static readonly string Staging     = "Staging";
    public static readonly string Production = "Production";
}

註冊服務

下面先介紹針對環境的服務註冊。ASP.NET Core應用提供了兩種服務註冊方式:第一種是調用IWebHostBuilder介面的ConfigureServices方法;第二種是調用UseStartup方法或者UseStartup<TStartup>方法註冊一個Startup類型,並在其ConfigureServices方法中完成服務註冊。對於第一種服務註冊方式,用於註冊服務的ConfigureServices方法具有一個參數類型為Action<WebHostBuilderContext, IServiceCollection>的重載,所以我們可以利用提供的WebHost
BuilderContext對象以如下所示的方式針對具體的環境註冊相應的服務。

class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
            .ConfigureServices((context,svcs)=> {
                if (context.HostingEnvironment.IsDevelopment())
                {
                    svcs.AddSingleton<IFoobar, Foo>();
                }
                else
                {
                    svcs.AddSingleton<IFoobar, Bar>();
                }
            }))
            .Build()
            .Run();
    } 
}

如果利用Startup類型來添加服務註冊,我們就可以按照如下所示的方式通過構造函數注入的方式得到所需的IWebHostEnvironment服務,並在ConfigureServices方法中根據它提供的環境資訊來註冊對應的服務。另外,Startup類型的ConfigureServices方法要麼是無參的,要麼具有一個類型為IServiceCollection的參數,所以我們無法直接在這個方法中注入IWebHost
Environment服務。

public class Startup
{
    private readonly IWebHostEnvironment _environment;
    public Startup(IWebHostEnvironment environment) => _environment = environment;
    public void ConfigureServices(IServiceCollection svcs)
    {
        if (_environment.IsDevelopment())
        {
            svcs.AddSingleton<IFoobar, Foo>();
        }
        else
        {
            svcs.AddSingleton<IFoobar, Bar>();
        }
    }
    public void Configure(IApplicationBuilder app) { }
}

除了在註冊Startup類型中的ConfigureServices方法完成針對承載環境的服務註冊,我們還可以將針對某種環境的服務註冊實現在對應的Configure{EnvironmentName}Services方法中。上面定義的Startup類型完全可以改寫成如下形式。

public class Startup
{
    public void ConfigureDevelopmentServices(IServiceCollection svcs)=> svcs.AddSingleton<IFoobar, Foo>();
    public void ConfigureServices(IServiceCollection svcs)=> svcs.AddSingleton<IFoobar, Bar>()
    public void Configure(IApplicationBuilder app) {}
}

註冊中間件

與服務註冊類似,中間件的註冊同樣具有兩種方式:一種是直接調用IWebHostBuilder介面的Configure方法;另一種則是調用註冊的Startup類型的同名方法。不管採用何種方式,中間件都是藉助IApplicationBuilder對象來註冊的。由於針對應用程式的IServiceProvider對象可以通過其ApplicationServices屬性獲得,所以我們可以利用它提供承載環境資訊的IWebHostEnvironment服務,進而按照如下所示的方式實現針對環境的中間件註冊。

class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
            .Configure(app=> {
                var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
                if (environment.IsDevelopment())
                {
                    app.UseMiddleware<FooMiddleware>();
                }
                app
                    .UseMiddleware<BarMiddleware>()
                    .UseMiddleware<BazMiddleware>();
            }))                       
            .Build()
            .Run();
    }
}

其實,用於註冊中間件的IApplicationBuilder介面還有UseWhen的擴展方法。顧名思義,這個方法可以幫助我們根據指定的條件來註冊對應的中間件。註冊中間件的前提條件可以通過一個Func<HttpContext, bool>對象來表示,對於某個具體的請求來說,只有對應的HttpContext對象滿足該對象設置的斷言,指定的中間件註冊操作才會生效。

public static class UseWhenExtensions
{
    public static IApplicationBuilder UseWhen(this IApplicationBuilder app,  Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration);
}

如果調用UseWhen方法來實現針對具體環境註冊對應的中間件,我們就可以按照如下所示的方式利用HttpContext來提供針對當前請求的IServiceProvider對象,進而得到承載環境資訊的IWebHostEnvironment服務,最終根據提供的環境資訊進行有針對性的中間件註冊。

class Program
{
    public static void Main()
    {
        Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder
            .Configure(app=> app
                .UseWhen(context=>context.RequestServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment(),
                    builder => builder.UseMiddleware<FooMiddleware>())
                .UseMiddleware<BarMiddleware>()
                .UseMiddleware<BazMiddleware>()))
            .Build()
            .Run();
    }
}

如果應用註冊了Startup類型,那麼針對環境的中間件註冊就更加簡單,因為用來註冊中間件的Configure方法自身是可以注入任意依賴服務的,所以我們可以在該方法中按照如下所示的方式直接注入IWebHostEnvironment服務來提供環境資訊。

public class Startup
{
    public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
    {
        if (environment.IsDevelopment())
        {
            app.UseMiddleware<FooMiddleware>();
        }
        app
            .UseMiddleware<BarMiddleware>()
            .UseMiddleware<BazMiddleware>();
    }
}

與服務註冊類似,針對環境的中間件註冊同樣可以定義在對應的Configure{EnvironmentName}方法中,上面這個Startp類型完全可以改寫成如下形式。

public class Startup
{
    public void ConfigureDevelopment (IApplicationBuilder app)
    {
        app.UseMiddleware<FooMiddleware>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app
            .UseMiddleware<BarMiddleware>()
            .UseMiddleware<BazMiddleware>();
    }
}

配置

上面介紹了針對環境的服務和中間件註冊,下面介紹如何根據當前的環境來提供有針對性的配置。通過前面的介紹可知,IWebHostBuilder介面提供了一個名為Configure
AppConfiguration的方法,我們可以調用這個方法來註冊相應的IConfigureSource對象。這個方法具有一個類型為Action<WebHostBuilderContext, IConfigurationBuilder>的參數,所以可以通過提供的這個WebHostBuilderContext上下文得到提供環境資訊的IWebHostEnvironment對象。

如果採用配置文件,我們可以將配置內容分配到多個文件中。例如,我們可以將與環境無關的配置定義在Appsettings.json文件中,然後針對具體環境提供對應的配置文件Appsettings.
{EnvironmentName}.json(如Appsettings.Development.json、Appsettings.Staging.json和Appsettings.
Production.json)。最終我們可以按照如下所示的方式將針對這兩類配置文件的IConfigureSource註冊到提供的IConfigurationBuilder對象上。

ASP.NET Core編程模式[1]:管道式的請求處理
ASP.NET Core編程模式[2]:依賴注入的運用
ASP.NET Core編程模式[3]:配置多種使用形式
ASP.NET Core編程模式[4]:基於承載環境的編程
ASP.NET Core編程模式[5]:如何放置你的初始化程式碼