1. 程式人生 > >ASP.NET Core應用基本程式設計模式[4]:基於承載環境的程式設計

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個與承載相關的屬性會以下圖所示的形式呈現在控制檯上。

由於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]:如何放置你的初始