1. 程式人生 > >DotNetCore深入了解之一Startup類

DotNetCore深入了解之一Startup類

collect 依據 hand loader exec typeof rom opened creates

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

技術分享圖片
1 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>();
View Code

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 }
View Code

可以看出所指定的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     }
View Code

所以明顯不是第一種方式,那麽其又是怎麽與第二種方式關聯起來的?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 }
View Code

它的內部處理中會找尋三種方法,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 }
View Code

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 }
View Code

使用第二種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 }
View Code

此外,在找尋各種方法的處理中可以看到環境變量的身影。所以用第二種方式可以依據不同的環境變量定義同類型但不同名稱的方法,這樣可以省去寫不少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 }
View Code

至於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 }
View Code

然後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 }
View Code

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 }
View Code

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 }
View Code

上述方法中HostingApplication實例最終被傳入KestrelServer的啟動方法中,這樣才能在其內部調用HostingApplication的ProcessRequestAsync方法,並開始層層調用諸多的RequestDelegate方法。

技術分享圖片
1 public Task ProcessRequestAsync(Context context)
2 {
3     return _application(context.HttpContext);
4 }
View Code

如果覺得使用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         });
View Code

不過使用獨立的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 }
View Code

DotNetCore深入了解之一Startup類