ASP.NET Core应用基本编程模式[5]:如何放置你的初始化代码
一个ASP.NET Core应用的核心就是由一个服务器和一组有序中间件组成的请求处理管道,服务器只负责监听、接收和分发请求,以及最终完成对请求的响应,所以一个ASP.NET Core应用针对请求的处理能力和处理方式由注册的中间件来决定。一个ASP.NET Core在启动过程中的核心工作就是注册中间件,本节主要介绍应用启动过程中以中间件注册为核心的初始化工作。
目录
一、Startup
二、IHostingStartup
三、IStartupFilter
一、Startup
由于ASP.NET Core应用承载于以IHost/IHostBuilder为核心的承载系统中,所以在启动过程中需要的所有操作都可以直接调用IHostBuilder接口相应的方法来完成,但是我们倾向于将这些代码单独定义在按照约定定义的Startup类型中。由于注册Startup的核心目的是注册中间件,所以Configure方法是必需的,用于注册服务的ConfigureServices方法和用来设置第三方依赖注入容器的ConfigureContainer方法是可选的。如下所示的代码片段体现了典型的Startup类型定义方式。
public class Startup { public void Configure(IApplicationBuilder app); public void ConfigureServices(IServiceCollection services); public void ConfigureContainer(FoobarContainerBuilder container); }
除了显式调用IWebHostBuilder接口的UseStartup方法或者UseStartup<TStartup>方法注册一个Startup类型,如果另外一个程序集中定义了合法的Startup类型,我们可以通过配置将它作为启动程序集。作为启动程序集的配置项目的名称为startupAssembly,对应静态类型WebHostDefaults的只读字段StartupAssemblyKey。
public static class WebHostDefaults { public static readonly string StartupAssemblyKey; ... }
一旦启动程序集通过配置的形式确定下来,系统就会试着从该程序集中找到一个具有最优匹配度的Startup类型。下面列举了一系列Startup类型的有效名称,Startup类型加载器正是按照这个顺序从启动程序集类型列表中进行筛选的,如果最终没有任何一个类型满足条件,那么系统会抛出一个InvalidOperationException异常。
- Startup{EnvironmentName}(全名匹配)。
- Startup(全名匹配)。
- {StartupAssembly}.Startup{EnvironmentName}(全名匹配)。
- {StartupAssembly}.Startup (全名匹配)。
- Startup{EnvironmentName}(任意命名空间)。
- Startup(任意命名空间)。
由此可以看出,当ASP.NET Core框架从启动程序集中定位Startup类型时会优先选择类型名称与当前环境名称相匹配的。为了使读者对这个选择策略有更加深刻的认识,下面做一个实例演示。我们利用Visual Studio创建一个名为App的控制台应用,并编写了如下这段简单的程序。在如下所示的代码片段中,我们将当前命令行参数作为配置源。我们既没有调用IWebHostBuilder接口的Configure方法注册任何中间件,也没有调用UseStartup方法或者UseStartup<TStartup>方法注册Startup类型。
class Program { static void Main(string[] args) { Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder => builder.ConfigureLogging(options => options.ClearProviders())) .Build() .Run(); } }
我们创建了另一个名为AppStartup的类库项目,并在其中定义了如下3个继承自抽象类StartupBase的类型。根据命名约定,StartupDevelopment类型和StartupStaging类型分别针对Development环境与Staging环境,而Startup类型则不针对某个具体的环境(环境中性)。
namespace AppStartup { public abstract class StartupBase { public StartupBase() => Console.WriteLine(this.GetType().FullName); public void Configure(IApplicationBuilder app) { } } public class StartupDevelopment : StartupBase { } public class StartupStaging: StartupBase { } public class Startup: StartupBase { } }
由于基类StartupBase的构造函数会将自身类型的全名输出到控制台上,所以可以根据这个输出确定哪个Startup类型会被选用。我们采用命令行的形式多次启动App应用,并以命令行参数的形式指定启动程序集名称和当前环境名称,控制台上呈现的输出结果如下图所示。如果没有显式指定环境名称,当前应用就会采用默认的Production环境名称,所以不针对具体环境的AppStartup.Startup被选择作为Startup类型。当我们将环境名称分别显式设置为Development和Staging之后,被选择作为Startup类型的分别为StartupDevelopment和StartupStaging。
与具体承载环境进行关联除了可以体现在Startup类型的命名(Startup{EnvironmentName})上,还可以体现在对方法的命名(Configure{EnvironmentName}、Configure{EnvironmentName}Services和Configure{EnvironmentName}Container)上。如下所示的这个Startup类型针对开发环境、预发环境和产品环境定义了对应的方法,如果还有其他的环境,不具有环境名称的3个方法将会被使用,在上面介绍服务注册和中间件注册时已经有明确的说明。
public class Startup { public void Configure(IApplicationBuilder app); public void ConfigureServices(IServiceCollection services); public void ConfigureContainer(FoobarContainerBuilder container); public void ConfigureDevelopment(IApplicationBuilder app); public void ConfigureDevelopmentServices(IServiceCollection services); public void ConfigureDevelopmentContainer(FoobarContainerBuilder container); public void ConfigureStaging(IApplicationBuilder app); public void ConfigureStagingServices(IServiceCollection services); public void ConfigureStagingContainer(FoobarContainerBuilder container); public void ConfigureProduction(IApplicationBuilder app); public void ConfigureProductionServices(IServiceCollection services); public void ConfigureProductionContainer(FoobarContainerBuilder container); }
二、IHostingStartup
除了通过注册Startup类型来初始化应用程序,我们还可以通过注册一个或者多个IHostingStartup服务达到类似的目的。由于IHostingStartup服务可以通过第三方程序集来提供,如果第三方框架、类库或者工具需要在应用启动时做相应的初始化工作,就可以将这些工作实现在注册的IHostingStart服务中。如下所示的代码片段是服务接口IHostingStartup的定义,它只定义了一个唯一的Configure方法,该方法可以利用输入参数得到当前使用的IWebHost
Builder对象。
public interface IHostingStartup { void Configure(IWebHostBuilder builder); }
IHostingStartup服务是通过如下所示的HostingStartupAttribute特性来注册的。从给出的定义可以看出这是一个针对程序集的特性,在构造函数中指定的就是注册的IHostingStartup类型。由于在同一个程序集中可以多次使用该特性(AllowMultiple=true),所以同一个程序集可以提供多个IHostingStartup服务类型。
[AttributeUsage((AttributeTargets) AttributeTargets.Assembly, Inherited=false, AllowMultiple=true)] public sealed class HostingStartupAttribute : Attribute { public Type HostingStartupType { get; } public HostingStartupAttribute(Type hostingStartupType); }
如果希望某个程序集提供的IHostingStartup服务类型能够真正应用到当前程序中,我们需要采用配置的形式对程序集进行注册。注册IHostingStartup程序集的配置项名称为hostingStartupAssemblies,对应静态类型WebHostDefaults的只读字段HostingStartupAssemblies
Key。通过配置形式注册的程序集名称以分号进行分隔。当前应用名称会作为默认的IHostingStartup程序集进行注册,如果针对IHostingStartup类型的注册定义在该程序集中,就不需要对该程序集进行显式配置。
public static class WebHostDefaults { public static readonly string HostingStartupAssembliesKey; public static readonly string PreventHostingStartupKey; public static readonly string HostingStartupExcludeAssembliesKey; }
这一特性还有一个全局开关。如果不希望第三方程序集对当前应用程序进行干预,我们可以通过配置项preventHostingStartup关闭这一特性,该配置项的名称对应WebHostDefaults的PreventHostingStartupKey属性。另外,对于布尔值类型的配置项,“true”(不区分大小写)和“1”都表示True,其他值则表示False。WebHostDefaults还通过HostingStartupExcludeAssembliesKey属性定义了另一个配置项,其名称为hostingStartupExcludeAssemblies,用于设置需要被排除的程序集列表。
下面通过对前面的程序略加修改来演示针对IHostingStartup服务的初始化。首先在App项目中定义了如下这个实现了IHostingStartup接口的类型Foo,它实现的Configure方法会在控制台上打印出相应的文字以确定该方法是否被调用。这个自定义的IHostingStartup服务类型通过HostingStartupAttribute特性进行注册。IHostingStartup相关的配置只有通过环境变量和调用IWebHostBuilder接口的UseSetting方法进行设置才有效,所以虽然我们采用命令行参数提供原始配置,但是必须调用UseSetting方法将它们应用到IWebHostBuilder对象上。
[assembly: HostingStartup(typeof(Foo))] class Program { static void Main(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) .Build(); Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureLogging(options => options.ClearProviders()) .UseSetting("hostingStartupAssemblies", config["hostingStartupAssemblies"]) .UseSetting("preventHostingStartup", config["preventHostingStartup"]) .Configure(app => app.Run(context => Task.CompletedTask))) .Build() .Run(); } } public class Foo : IHostingStartup { public void Configure(IWebHostBuilder builder) => Console.WriteLine("Foo.Configure()"); }
另一个AppStartup项目包含如下两个自定义的IHostingStartup服务类型Bar和Baz,我们采用如下方式利用HostingStartupAttribute特性对它们进行了注册。
[assembly: HostingStartup(typeof(Bar))] [assembly: HostingStartup(typeof(Baz))] public abstract class HostingStartupBarBase : IHostingStartup { public void Configure(IWebHostBuilder builder) => Console.WriteLine($"{GetType().Name}.Configure()"); } public class Bar : HostingStartupBarBase {} public class Baz : HostingStartupBarBase {}
我们采用命令行以下图所示的形式两次启动App应用。对于第一次应用启动,由于对启动程序集AppStartup进行了显式设置,由它提供的两个IHostingStartup服务(Bar和Baz)都得以正常执行。而注册的IHostingStartup服务Foo,由于被注册到当前应用程序对应的程序集,虽然我们没有将它显式地添加到启动程序集列表中,但它依然会执行,而且是在其他程序集注册的IHostingStartup服务之前执行。至于第二次应用启动,由于我们通过命令行参数关闭了针对IHostingStartup服务的初始化功能,所以Foo、Bar和Baz这3个自定义IHostingStartup服务都不会执行。
三、IStartupFilter
中间件的注册离不开IApplicationBuilder对象,注册的IStartup服务的Configure方法会利用该对象帮助我们完成中间件的构建与注册。调用IWebHostBuilder接口的Configure方法时,系统会注册一个类型为DelegateStartup的IStartup服务,DelegateStartup会利用提供的Action<IApplicationBuilder>对象完成中间件的构建与注册。
如果调用IWebHostBuilder接口的UseStartup方法或者UseStartup<Startup>方法注册了一个Startup类型并且该类型没有实现IStartup接口,系统就会按照约定规则创建一个类型为ConventionBasedStartup的IStartup服务。如果注册的Startup类型实现了IStartup接口,意味着注册的就是IStartup服务。
除了采用上述两种方式利用系统提供的IStartup服务来注册中间件,我们还可以通过注册IStartupFilter服务来达到相同的目的。一个应用程序可以注册多个IStartupFilter服务,它们会按照注册的顺序组成一个链表。IStartupFilter接口具有如下所示的唯一方法Configure,中间件的注册体现在它返回的Action<IApplicationBuilder>对象上,而作为唯一参数的Action<IApplication
Builder>对象则代表了针对后续中间件的注册。
public interface IStartupFilter { Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next); }
虽然注册中间件是IStartup对象和IStartupFilter对象的核心功能,但是两者之间还是不尽相同的,它们之间的差异在于:IStartupFilter对象的Configure方法会在IStartup对象的Configure方法之前执行。正因为如此,如果需要将注册的中间件前置或者后置,就需要利用IStartupFilter对象来注册它们。
接下来我们同样会演示一个针对IStartupFilter的中间件注册的实例。首先定义如下两个中间件类型FooMiddleware和BarMiddleware,它们派生于同一个基类StringContentMiddleware。当InvokeAsync方法被执行时,中间件在将请求分发给后续中间件之前和之后会分别将一段预先指定的文字写入响应消息的主体内容中,它们代表了中间件针对请求的前置和后置处理。
public abstract class StringContentMiddleware { private readonly RequestDelegate _next; private readonly string _preContents; private readonly string _postContents; public StringContentMiddleware(RequestDelegate next, string preContents, string postContents) { _next = next; _preContents = preContents; _postContents = postContents; } public async Task InvokeAsync(HttpContext context) { await context.Response.WriteAsync(_preContents); await _next(context); await context.Response.WriteAsync(_postContents); } } public class FooMiddleware : StringContentMiddleware { public FooMiddleware (RequestDelegate next) : base(next, "Foo=>", "Foo") { } } public class BarMiddleware : StringContentMiddleware { public BarMiddleware (RequestDelegate next) : base(next, "Bar=>", "Bar=>") { } }
可以采用如下方式对FooMiddleware和BarMiddleware这两个中间件进行注册。具体来说,我们为中间件类型FooMiddleware创建了一个自定义的IStartupFilter类型FooStartupFilter,FooStartupFilter实现的Configure方法中注册了这个中间件。FooStartupFilter最终通过IWebHostBuilder接口的ConfigureServices方法进行注册。至于中间件类型BarMiddleware,我们调用IWebHostBuilder接口的Configure方法对它进行注册。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureServices(svcs => svcs.AddSingleton<IStartupFilter, FooStartupFilter>()) .Configure(app => app .UseMiddleware<BarMiddleware>() .Run(context => context.Response.WriteAsync("...=>")))) .Build() .Run(); } } public class FooStartupFilter : IStartupFilter { public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) { return app => { app.UseMiddleware<FooMiddleware>(); next(app); }; } }
由于IStartupFilter的Configure方法会在IStartup的Configure方法之前执行,所以对于最终构建的请求处理管道来说,FooMiddleware中间件置于BarMiddleware中间件前面。换句话说,当管道在处理某个请求的过程中,FooMiddleware中间件的前置请求处理操作会在BarMiddleware中间件之前执行,而它的后置请求处理操作则在BarMiddleware中间件之后执行。在启动这个程序之后,如果利用浏览器对该应用发起请求,得到的输出结果如下图所示。
ASP.NET Core编程模式[1]:管道式的请求处理
ASP.NET Core编程模式[2]:依赖注入的运用
ASP.NET Core编程模式[3]:配置多种使用形式
ASP.NET Core编程模式[4]:基于承载环境的编程
ASP.NET Core编程模式[5]:如何放置你的初始化代码