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]:如何放置你的初始化代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM