学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup

Posted tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup相关的知识,希望对你有一定的参考价值。

一个ASP.NET Core应用被启动之后就具有了针对请求的处理能力,而这个能力是由管道赋予的,所以应用的启动同时意味着管道的成功构建。由于管道是由注册的服务器和若干中间件构成的,所以应用启动过程中一个核心的工作就是完成中间节的注册。由于依赖注入在ASP.NET Core应用这得到非常广泛的应用,框架绝大部分的工作都会分配给我们预先注册的服务,所以服务注册也是启动WebHost过程的另一项核心工作。这两项在启动过程中必须完成的核心工作通过一个名为Startup的对象来承载。 [本文已经同步到《ASP.NET Core框架揭秘》之中]

目录

一、 DelegateStartup
二、ConventionBasedStartup
    StartupMethods
    StartupLoader
    如何选择启动类型
    如何选择服务注册方法和中间件注册方法
    StartupMethods对象的创建
    UseStartup方法究竟做了些什么?
三、选择哪一个Startup

这里所谓的Startup实际上是对所有实现了IStartup接口的所有类型以及对应对象的统称。如下面的代码片段所示,服务注册由ConfigureServices方法来实现,它返回一个ServiceProvider对象,至于另一个方法Configure则负责完成中间件的注册,方法输入参数是一个ApplicationBuilder对象。

   1: public interface IStartup
   2: {
   3:     IServiceProvider ConfigureServices(IServiceCollection services);
   4:     void Configure(IApplicationBuilder app);
   5: }

IStartup接口所在的NuGet包中还定义了另一个实现了这个接口的抽象类StartupBase。如下面的代码片段所示,StartupBase实现了抽象方法ConfigureServices,该方法直接利用提供的ServiceCollection对象创建返回的ServiceProvider。换句话说,派生于StartupBase的Startup类型如果没用重写ConfigureServices方法,它们实际上只关心中间件的注册,而不需要注册额外的服务。

   1: public abstract class StartupBase : IStartup
   2: {
   3:     public abstract void Configure(IApplicationBuilder app);
   4:     public virtual IServiceProvider ConfigureServices(IServiceCollection services)
   5:     {
   6:         return services.BuildServiceProvider();
   7:     }
   8: }

一、 DelegateStartup

我们来想想具体的应用中是如何注册中间件和服务的。中间件的注册可以采用两种方式,最简单的方式就是直接调用IWebHostBuilder的Configure方法。如下面的代码片段所示,这个方法直接上是借助于一个类型为Action<IApplicationBuilder>的委托对象将中间件注册到提供的ApplicationBuilder对象上。

   1: public static class WebHostBuilderExtensions
   2: {
   3:     public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp);
   4: }
   5:  
   6: new WebHostBuilder()
   7:     .Configure(app => app
   8:         .UseExceptionHandler("/Home/Error")
   9:         .UseStaticFiles()
  10:         .UseIdentity()
  11:         .UseMvc())
  12:

如果我们在应用中通过调用上面这个Configure方法来注册所需的中间件,WebHost在启动的时候会创建一个类型为DelegateStartup的Startup对象来完成真正的中间件注册工作。如下面的代码片段所示,DelegateStartup派生于StartupBase这个抽象类,它利用一个在构造时提供的Action<IApplicationBuilder>对象实现了Configure方法。很明显,我们调用IWebHostBuilder的Configure方法指定的Action<IApplicationBuilder>对象将用来创建这个DelegateStartup对象。

   1: public class DelegateStartup : StartupBase
   2: {
   3:     private Action<IApplicationBuilder> _configureApp;
   4:  
   5:     public DelegateStartup(Action<IApplicationBuilder> configureApp)
   6:     {
   7:         _configureApp = configureApp;
   8:     }
   9:  
  10:     public override void Configure(IApplicationBuilder app)
  11:     {
  12:         _configureApp(app);
  13:     }
  14: }

如下的代码片段体现了 IWebHostBuilder的扩展方法Configure的实现逻辑。如下面的代码片段所示,这个方法根据提供的Action<IApplicationBuilder>对象创建了一个DelegateStartup对象,并调用ConfigureServices方法以淡例模式注册到WebHostBuilder的服务集合中。这段代码还体现了另一个细节,除了进行Startup的服务注册之外,该方法还对“ApplicationName”选项(对应WebHostOptions的ApplicationName)进行了设置。

   1: public static class WebHostBuilderExtensions
   2: {    
   3:     public static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Action<IApplicationBuilder> configureApp)
   4:     {
   5:         var startupAssemblyName = configureApp.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name;
   6:         return hostBuilder
   7:             .UseSetting("applicationName", startupAssemblyName)
   8:             .ConfigureServices(svcs => svcs.AddSingleton<IStartup>(new DelegateStartup(configureApp));
   9:     }
  10: }

二、ConventionBasedStartup

我们知道应用中最常见的服务和中间件注册代码都定义在一个单独的类中,通常直接将其命名为Startup。为了与IStartup接口代表的Startup相区别,我们使用 “启动类(型)” 来称呼这个类。按照约定,启动类中会分别定义一个ConfigureServices和Configure方法来注册服务和中间件。一般情况下,这样的类型一般需要通过调用UseStartup<T>这个扩展方法注册到WebHostBuilder上。

   1: public class Startup
   2: {
   3:     public void ConfigureServies(IServiceCollection services);
   4:     public void Configure(IApplicationBuilder app);
   5: }
   6:  
   7: new WebHostBuilder()
   8:     .UseStartup<Startup>()
   9:

如果我们在应用中将服务和中间件注册的实现定义在启动类型中,当WebHost被启动的时候,ASP.NET Core会创建一个类型为ConventionBasedStartup的Startup对象。这个Startup类型之所以采用这样的命名方式,是因为ASP.NET Core并没有采用接口实现的方式为启动类型做强制性的约束,而仅仅是为作为启动类型的定义提供了一个约定而已,至于具体采用怎样的约定,我们将在后续部分进行详细介绍。

StartupMethods

在了解了启动类型的约定以及常见的定义形式之外,我们现在来讨论这对这个类型创建的ConventionBasedStartup就是怎样的对象。从下面的代码片段可以看出,一个ConventionBasedStartup对象是根据一个类型为StartupMethods对象创建的。顾名思义,StartupMethods只在提供两个用户注册服务和中间件的方法,这两个方法体现在由它的两个属性(ConfigureServicesDelegate和ConfigureDelegate)提供的两个委托对象,这两个委托对象分别实现了定义在ConventionBasedStartup的ConfigureServices和Configure方法。

   1: public class ConventionBasedStartup : IStartup
   2: {
   3:     public ConventionBasedStartup(StartupMethods methods);
   4:     public IServiceProvider ConfigureServices(IServiceCollection services); 
   5:     public void Configure(IApplicationBuilder app);
   6: }
   7:  
   8: public class StartupMethods
   9: {      
  10:     public Func<IServiceCollection, IServiceProvider>     ConfigureServicesDelegate {  get; }
  11:     public Action<IApplicationBuilder>                    ConfigureDelegate {  get; }
  12:  
  13:     public StartupMethods(Action<IApplicationBuilder> configure);
  14:     public StartupMethods(Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices);   
  15: }

StartupLoader

既然ConventionBasedStartup对象是根据提供的一个StartupMethods对象创建的,那么现在的核心问题则变成了这个StartupMethods对象如何根据启动类型创建的。StartupMethods的创建者是一个类型为StartupLoader的对象,如下面的代码片段所示,StartupLoader定了两个名为FindStartupType和LoadMethods静态方法,前者用于启动类型的解析,后者则实现了StartupMethods对象的创建。

   1: public class StartupLoader
   2: {
   3:     public static Type FindStartupType(string startupAssemblyName, string environmentName);
   4:     public static StartupMethods LoadMethods(IServiceProvider services, Type startupType, string environmentName);
   5: }

如何选择启动类型

如果启动类型没有通过调用WebHostBuilder的如下两个扩展方法UseStartup/UseStartup<TStartup>的显式注册,那么StartupLoader的FindStartupType方法会被调用来解析出正确的启动类型。这个方法具有两个参数,分别代表启动类型所在的程序集名称和当前环境名称,它们实际上对应着WebHostOptions的两个同名属性。当FindStartupType方法被执行并成功加载了提供的程序集之后,它会按照约定的启动类型全名从该程序集中加载启动类型,候选的启动类型全名按照选择优先级排列如下:

  • Startup{EnvironmentName} (无命名空间)
  • {StartupAssemblyName}.Startup{EnvironmentName}
  • Startup(无命名空间)
  • {StartupAssemblyName}.Startup{EnvironmentName}
  • **. Startup{EnvironmentName}(任意命名空间)
  • **. Startup(任意命名空间)

这个列表体现了启动类型解析过程中选择有效类型名称的一个基本策略,即“环境名称优先”和“无命名空间优先”。我们可以通过一个简单的实例来证明这个策略的存在。我们在一个ASP.NET Core控制台应用中添加一个名为“StartupLib”(程序集也采用这个名称)的类库项目,然后在这个项目中定义如下两组启动类,其中一组具有命名空间,另一组则采用程序集名称作为命名空间。这些启动类都派生于我们自定义的基类StartupBase,后者的Configure方法中注册了一个中间件将自身的类型作为响应内容。对于每组中的三个启动类,一个命名为Startup,另外两个则分别以环境名称( “Development” 和 “Production” )作为后缀。

   1: public class StartupBase
   2: {
   3:     public void ConfigureServices(IServiceCollection services){}
   4:     public void Configure(IApplicationBuilder app)
   5:     {
   6:         app.Run(async context => await context.Response.WriteAsync(this.GetType().FullName));
   7:     }
   8: }
   9:  
  10: public class Startup                : StartupBase{}
  11: public class StartupDevelopment     : StartupBase{}
  12: public class StartProduction        : StartupBase{}
  13:  
  14: namespace StartupLib
  15: {
  16:     public class Startup               : StartupBase{}
  17:     public class StartupDevelopment    : StartupBase{}
  18:     public class StartProduction       : StartupBase{}
  19: }

我们采用如下的程序来启动一个ASP.NET Core应用。如下面的代码代码片段所示,我们在利用WebHostBuilder创建并启动WebHost之前,调用UseSettings方法以配置的形式指定了启动程序集(“StartupLib”)和当前运行环境(“Development”)的名称。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseSetting("startupAssembly", "StartupLib")
   8:             .UseSetting("environment", "Development")
   9:             .Build()
  10:             .Run();
  11:     }
  12: }

根据上述的启动类型解析规则,对于六个候选的启动类型,最终被选择的是不具有命名空间的StartupDevelopment类型。当应用启动之后,我们利用浏览器请求应用监听地址(“http://localhost:5000”),这个被选择的启动程序的名称将会以如下的形式直接显示出来。

8

如何选择服务注册方法和中间件注册方法

在了解了ASP.NET Core针对启动类型命名的约定之后,我们来讨论一下定义在启动类中用于注册服务和中间件的两个方法的约定。这两个方法可以是静态方法,也可以是实例方法。从方法命名来看,这两个方法除了命名为“ConfigureServices”和“Configure”之外,方法名还可以携带运行环境名称,具体采用的格式分别为“Configure{EnvironmentName}Services”和“Configure{EnvironmentName}”,后者具有更高的选择优先级。

ConfigureServices/Configure{EnvironmentName}Services方法具有一个类型为IServiceCollection接口的参数,表示存放注册服务的ServiceCollection对象。如过这个方法没有定义任何参数,它依然是合法的。一般来说,这个方法不具有返回值(返回类型为void),但是它也可以定义成一个返回类型为IServiceProvider的方法。如果这个方法返回一个ServiceProvider对象,后续过程中获取的所有服务将从这个ServiceProvider中提取。对于没有返回值的情况,系统会根据当前注册的服务创建一个ServiceProvider。

Configure/Configure{EnvironmentName}方法只要求只要求第一个参数类型采用IApplicationBuilder接口,至于这个方法可以包含多少个参数,各个参数应该具有怎样的类型,并未做任何规定。实际上我们为这个方法定义任意后续参数都是合法的。当ConventionBasedStartup在调用这个方法的时候,同样是采用依赖注入的方式来提供这些参数。如下面的代码片段所示,我们为启动类的Configure方法定义相应的参数来直接使用在ConfigureServices方法上注册的三个服务。

   1: new WebHostBuilder()
   2:     .ConfigureServices(services => services.AddSingleton<IFoobar, Foobar>())
   3:
   4:  
   5: public class Startup
   6: {
   7:     public Startup(IFoobar foobar)
   8:     {
   9:         Debug.Assert(foobar.GetType() == typeof(Foobar));
以上是关于学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup的主要内容,如果未能解决你的问题,请参考以下文章

学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup

学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位

学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter

学网络通信,怎能不了解 Netty?

ASP.NET Core入门五

ASP.NET Core学习——2