為什么我們可以在Startup這個 “孤零零的” 類中配置依賴注入和管道?
它是什么時候被實例化並且調用的?
參數中的IServiceCollection services是怎么來的?
處理管道是怎么構建起來的?
啟動過程中,系統“默默的”做了哪些准備工作?
上一篇文章講了ASP.NET Core中的依賴注入(系列目錄), 而它的配置是在Startup這個文件中的 ConfigureServices(IServiceCollection services) 方法,而且Startup這個類也沒有繼承任何類或者接口。 深入的想一想,可能會冒出類似上面列出的好多問題,下面用一幅圖來看透它。
一、整體流程圖
先上圖, 覺得看不清可以點擊看大圖或者下載后放大查看。
圖一 (點擊放大)
二、WebHostBuilder
應用程序在Main方法之后通過調用CreateDefaultBuilder方法創建並配置WebHostBuilder,
1 public class WebHostBuilder : IWebHostBuilder 2 { 3 private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates; 4 5 private IConfiguration _config; 6 public IWebHostBuilder UseSetting(string key, string value) 7 { 8 _config[key] = value; 9 return this; 10 } 22 public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices) 23 { 24 if (configureServices == null) 25 { 26 throw new ArgumentNullException(nameof(configureServices)); 27 } 29 _configureServicesDelegates.Add(configureServices); 30 return this; 31 } 32 }
WebHostBuilder存在一個重要的集合① private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates; , 通過 ConfigureServices 方法將需要的Action加入進來。
UseSetting是一個用於設置Key-Value的方法, 一些常用的配置均會通過此方法寫入_config中。
三、UseStartup<Startup>()
CreateDefaultBuilder之后調用UseStartup<Startup>(),指定Startup為啟動類。
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) { var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name; return hostBuilder .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName) .ConfigureServices(services => { if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) { services.AddSingleton(typeof(IStartup), startupType); } else { services.AddSingleton(typeof(IStartup), sp => { var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>(); return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)); }); } }); }
首先獲取Startup類對應的AssemblyName, 調用UseSetting方法將其設置為WebHostDefaults.ApplicationKey(“applicationName”)的值。
然后調用WebHostBuilder的②ConfigureServices方法,將一個Action寫入WebHostBuilder 的 configureServicesDelegates中。
這個Action的意思就是說,如果這個被指定的類startupType是一個實現了IStartup的類, 那么將其通過AddSingleton注冊到services 這個ServiceCollection中, 如果不是, 那么將其“轉換”成 ConventionBasedStartup 這個實現了 IStartup的類后再進行注冊。這里涉及到一個StartupLoader的LoadMethods()方法,會通過字符串的方式查找“ConfigureServices”、“Configure{ environmentName}Services”這樣的方法。
注意:這里只是將一個Action寫入了configureServicesDelegates, 而不是已經執行了對IStartup的注冊, 因為這個Action尚未執行,services也還不存在。就像菩薩對八戒說: 八戒(Startup)你先在高老庄等着吧, 將來有個和尚帶領一個取經小分隊(ServiceCollection services )過來的時候你加入他們。
其實在CreateDefaultBuilder方法中的幾個UseXXX的方法也是這樣通過ConfigureServices將對應的Action寫入了configureServicesDelegates, 等待唐僧的到來。
四、WebHostBuilder.Build()
創建並配置好的WebHostBuilder開始通過Build方法創建WebHost了, 首先是BuildCommonServices,
1 private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors) 2 { 3 //...省略... 4 var services = new ServiceCollection(); 5 services.AddSingleton(_hostingEnvironment); 6 services.AddSingleton(_context); 7 //....各種Add.... 9 foreach (var configureServices in _configureServicesDelegates) 10 { 11 configureServices(_context, services); 12 } 14 return services; 15 }
在這個方法里創建了ServiceCollection services(以唐僧為首的取經小分隊), 然后通過各種Add方法注冊了好多內容進去(收了悟空),然后③foreach 之前暫存在configureServicesDelegates中的各個Action,傳入services逐一執行, 將之前需要注冊的內容注冊到services中, 這里就包括Startup(八戒),注意這里僅是進行了注冊,而未執行Startup的方法。
處理好的這個services被BuildCommonServices返回后賦值給 hostingServices,然后 hostingServices經過Clone()生成 applicationServices,再由這個 applicationServices進行 GetProviderFromFactory(hostingServices)生成一個 IServiceProvider hostingServiceProvider.經過一系列的處理后,可以創建WebHost了。
var host = new WebHost( applicationServices, hostingServiceProvider, _options, _config, hostingStartupErrors); host.Initialize();
將生成的applicationServices 和 hostingServiceProvider作為參數傳遞給新生成的WebHost。接下來就是這個WebHost的 Initialize()。
五、WebHost.Initialize()
WebHost的 Initialize()的主要工作就是BuildApplication()。
EnsureApplicationServices(): 用來處理WebHost的 private IServiceProvider _applicationServices ,④Startup的ConfigureServices方法在這里被調用。
_startup = _hostingServiceProvider.GetRequiredService<IStartup>();
_applicationServices = _startup.ConfigureServices(_applicationServiceCollection);
通過 GetRequiredService<IStartup>() 獲取到我們的_startup, 然后調用這個_startup的 ⑤ConfigureServices 方法,這就是我們用於依賴注入的startup類的ConfigureServices方法了。
所以,_applicationServices是根據_applicationServiceCollection 加上我們在_startup中注冊的內容之后重新生成的 IServiceProvider。
EnsureServer()⑥:通過 GetRequiredService<IServer>()獲取Server並配置監聽地址。
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>(); var builder = builderFactory.CreateBuilder(Server.Features); builder.ApplicationServices = _applicationServices;
獲取到 IApplicationBuilderFactory並通過它⑦創建 IApplicationBuilder,並將上面創建的_applicationServices賦值給它的ApplicationServices,它還有個重要的集合_components
private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
從_components的類型可以看出它其實是中間件的集合,是該調用我們的_startup的Configure方法的時候了。
先獲取定義的IStartupFilter, ⑧foreach這些IStartupFilter並與_startup的Configure方法一起將配置的中間件寫入_components,然后通過 Build()創建RequestDelegate _application,
在Build()中對_components進行處理生成請求處理管道,關於IStartupFilter和生成管道這部分將在下篇文章進行詳細說明。
六、WebHost.Run()
WebHost創建完畢, 最后一步就是Run起來了,WebHost的Run()會調用它的方法StartAsync()
public virtual async Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) { //......var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory); await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false); //..... }
在之前的文章中我們知道,請求是經過 Server監聽=>處理成httpContext=>Application處理,所以這里首先傳入上面創建的_application和一個httpContextFactory來⑨生成一個HostingApplication,並將這個HostingApplication傳入Server的StartAsync(), 當Server監聽到請求之后, 后面的工作由HostingApplication來完成。
⑩hostedServiceExecutor.StartAsync()方法用來開啟一個后台運行的服務,一些需要后台運行的操作比如定期刷新緩存等可以放到這里來。
七、更新
感謝dudu的留言,去github上看了一下WebHost的最新源碼,BuildApplication()不再包含EnsureApplicationServices()的調用,並且轉移到了WebHost.StartAsync() 中進行; WebHost.Initialize() 中由原本調用BuildApplication()改為調用原本放在BuildApplication()中調用的EnsureApplicationServices()。
通過VS加載符號的方式調試獲取到的WebHost仍是原來的版本,即使刪除下載的文件后再次重新獲取也一樣, 應該是和新建項目默認引用的依賴版本有關。