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