一個典型的ASP.NET Core應用程序會包含Program與Startup兩個文件。Program類中有應用程序的入口方法Main,其中的處理邏輯通常是創建一個WebHostBuilder,再生成WebHost,然后啟動項目。

1 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>();
UseStartup方法的實現內容:

1 public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) 2 { 3 var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; 4 5 return hostBuilder 6 .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) 7 .ConfigureServices(services => 8 { 9 if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) 10 { 11 services.AddSingleton(typeof(IStartup), startupType); 12 } 13 else 14 { 15 services.AddSingleton(typeof(IStartup), sp => 16 { 17 var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>(); 18 return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)); 19 }); 20 } 21 }); 22 }
可以看出所指定的Startup類型會在DI容器中注冊為單例形式,注冊的處理過程被封裝成Action
同時Startup類型可以有兩種實現方式:
- 自定義實現IStartup接口的類
- 內部定義的ConventionBasedStartup
實際使用的Startup類經常是這樣的:

1 public class Startup 2 { 3 public Startup(IConfiguration configuration) 4 { 5 Configuration = configuration; 6 } 7 8 public IConfiguration Configuration { get; } 9 10 // This method gets called by the runtime. Use this method to add services to the container. 11 public void ConfigureServices(IServiceCollection services) 12 { 13 } 14 15 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 16 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 17 { 18 } 19 }
所以明顯不是第一種方式,那么其又是怎么與第二種方式關聯起來的?ConventionBasedStartup的構造方法中傳入了StartupMethods類型的參數,它的LoadMethod方法里曝露了更多的信息。

1 public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName) 2 { 3 var configureMethod = FindConfigureDelegate(startupType, environmentName); 4 5 var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName); 6 var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName); 7 8 object instance = null; 9 if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic)) 10 { 11 instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType); 12 } 13 14 // The type of the TContainerBuilder. If there is no ConfigureContainer method we can just use object as it's not 15 // going to be used for anything. 16 var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object); 17 18 var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance( 19 typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type), 20 hostingServiceProvider, 21 servicesMethod, 22 configureContainerMethod, 23 instance); 24 25 return new StartupMethods(instance, configureMethod.Build(instance), builder.Build()); 26 }
它的內部處理中會找尋三種方法,ConfigureServices, Configure與ConfigureContainer。

1 private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName) 2 { 3 var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true); 4 return new ConfigureBuilder(configureMethod); 5 } 6 7 private static ConfigureContainerBuilder FindConfigureContainerDelegate(Type startupType, string environmentName) 8 { 9 var configureMethod = FindMethod(startupType, "Configure{0}Container", environmentName, typeof(void), required: false); 10 return new ConfigureContainerBuilder(configureMethod); 11 } 12 13 private static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName) 14 { 15 var servicesMethod = FindMethod(startupType, "Configure{0}Services", environmentName, typeof(IServiceProvider), required: false) 16 ?? FindMethod(startupType, "Configure{0}Services", environmentName, typeof(void), required: false); 17 return new ConfigureServicesBuilder(servicesMethod); 18 }
ConfigureServices方法用於在容器中注冊各種所需使用的服務接口類型,Configure方法中可以使用各種middleware(中間件),對HTTP請求pipeline(管道)進行配置。
這二者與IStartup接口的方法基本一致,而ConfigureContainer方法則是提供了一種引入第三方DI容器的功能。

1 public interface IStartup 2 { 3 IServiceProvider ConfigureServices(IServiceCollection services); 4 5 void Configure(IApplicationBuilder app); 6 }
使用第二種Startup類型的實現方式時,對於Configure方法的參數也可以更加靈活,不僅可以傳入IApplicationBuilder類型,還可以有其它已注冊過的任意接口類型。
ConfigureBuilder類的Build方法對額外參數的處理方法簡單明了,是IApplicationBuilder類型的直接傳入參數實例,不是的則從DI容器中獲取實例:

1 public class ConfigureBuilder 2 { 3 public Action<IApplicationBuilder> Build(object instance) => builder => Invoke(instance, builder); 4 5 private void Invoke(object instance, IApplicationBuilder builder) 6 { 7 // Create a scope for Configure, this allows creating scoped dependencies 8 // without the hassle of manually creating a scope. 9 using (var scope = builder.ApplicationServices.CreateScope()) 10 { 11 var serviceProvider = scope.ServiceProvider; 12 var parameterInfos = MethodInfo.GetParameters(); 13 var parameters = new object[parameterInfos.Length]; 14 for (var index = 0; index < parameterInfos.Length; index++) 15 { 16 var parameterInfo = parameterInfos[index]; 17 if (parameterInfo.ParameterType == typeof(IApplicationBuilder)) 18 { 19 parameters[index] = builder; 20 } 21 else 22 { 23 try 24 { 25 parameters[index] = serviceProvider.GetRequiredService(parameterInfo.ParameterType); 26 } 27 ... 28 } 29 } 30 MethodInfo.Invoke(instance, parameters); 31 } 32 } 33 }
此外,在找尋各種方法的處理中可以看到環境變量的身影。所以用第二種方式可以依據不同的環境變量定義同類型但不同名稱的方法,這樣可以省去寫不少if...else...
的處理。UseStartup方法中還只是申明了需要注冊Startup類型,實際的調用是在WebHostBuilder類執行Build方法時發生的。

1 private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors) 2 { 3 ... 4 5 foreach (var configureServices in _configureServicesDelegates) 6 { 7 configureServices(_context, services); 8 } 9 10 return services; 11 }
至於Startup類型中方法的調用時機,則需跟蹤到WebHost類中。
首先是它的Initialize方法會確保獲得Startup類的實例,並調用ConfigureServices方法注冊服務接口。

1 private void EnsureApplicationServices() 2 { 3 if (_applicationServices == null) 4 { 5 EnsureStartup(); 6 _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); 7 } 8 } 9 10 private void EnsureStartup() 11 { 12 if (_startup != null) 13 { 14 return; 15 } 16 17 _startup = _hostingServiceProvider.GetService<IStartup>(); 18 19 ... 20 }
然后WebHost實例被啟動時,它的BuildApplication方法會創建一個ApplicationBuilder實例,以其作為Configure方法參數,同時調用Configure方法,這時Configure方法中對那些middleware的處理方式(即Func<RequestDelegate, RequestDelegate>方法)會被加入到ApplicationBuilder之中。

1 private RequestDelegate BuildApplication() 2 { 3 try 4 { 5 _applicationServicesException?.Throw(); 6 EnsureServer(); 7 8 var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>(); 9 var builder = builderFactory.CreateBuilder(Server.Features); 10 builder.ApplicationServices = _applicationServices; 11 12 var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>(); 13 Action<IApplicationBuilder> configure = _startup.Configure; 14 foreach (var filter in startupFilters.Reverse()) 15 { 16 configure = filter.Configure(configure); 17 } 18 19 configure(builder); 20 21 return builder.Build(); 22 } 23 }
ApplicationBuilder的Build方法將這些處理邏輯嵌套起來,最底層的是返回404未找到的處理邏輯。

1 public RequestDelegate Build() 2 { 3 RequestDelegate app = context => 4 { 5 context.Response.StatusCode = 404; 6 return Task.CompletedTask; 7 }; 8 9 foreach (var component in _components.Reverse()) 10 { 11 app = component(app); 12 } 13 14 return app; 15 }
BuildApplication方法返回已嵌套的RequestDelegate委托方法,並在之后生成的HostingApplication實例中,將其傳入它的構造方法。

1 public virtual async Task StartAsync(CancellationToken cancellationToken = default) 2 { 3 HostingEventSource.Log.HostStart(); 4 _logger = _applicationServices.GetRequiredService<ILogger<WebHost>>(); 5 _logger.Starting(); 6 7 var application = BuildApplication(); 8 9 _applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime; 10 _hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>(); 11 var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticListener>(); 12 var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>(); 13 var hostingApp = new HostingApplication(application, _logger, diagnosticSource, httpContextFactory); 14 await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); 15 16 }
上述方法中HostingApplication實例最終被傳入KestrelServer的啟動方法中,這樣才能在其內部調用HostingApplication的ProcessRequestAsync方法,並開始層層調用諸多的RequestDelegate方法。

1 public Task ProcessRequestAsync(Context context) 2 { 3 return _application(context.HttpContext); 4 }
如果覺得使用Startup類還是有點麻煩的話,直接使用WebHostBuilder所提供的擴展方法也是同樣的效果。

1 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 2 WebHost.CreateDefaultBuilder(args) 3 .ConfigureAppConfiguration((hostingContext, config) => 4 { 5 HostingEnvironment = hostingContext.HostingEnvironment; 6 Configuration = config.Build(); 7 }) 8 .ConfigureServices(services => 9 { 10 services.AddMvc(); 11 }) 12 .Configure(app => 13 { 14 if (HostingEnvironment.IsDevelopment()) 15 { 16 app.UseDeveloperExceptionPage(); 17 } 18 else 19 { 20 app.UseExceptionHandler("/Error"); 21 } 22 23 app.UseMvcWithDefaultRoute(); 24 app.UseStaticFiles(); 25 });
不過使用獨立的Startup類有着額外的好處,Startup類可以被包含在與Program類不用的的程序集中。然后通過WebHostOptions類中StartupAssembly屬性的設定及其它相關處理,完成UseStartup方法同樣的功能。

1 private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors) 2 { 3 ... 4 5 if (!string.IsNullOrEmpty(_options.StartupAssembly)) 6 { 7 try 8 { 9 var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName); 10 11 if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) 12 { 13 services.AddSingleton(typeof(IStartup), startupType); 14 } 15 else 16 { 17 services.AddSingleton(typeof(IStartup), sp => 18 { 19 var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>(); 20 var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName); 21 return new ConventionBasedStartup(methods); 22 }); 23 } 24 } 25 ... 26 } 27 28 ... 29 30 return services; 31 }