我們一致在說 ASP.NET Core廣泛地使用到了依賴注入,通過前面兩個系列的介紹,相信讀者朋友已經體會到了這一點。由於前面兩章已經涵蓋了依賴注入在管道構建過程中以及管道在處理請求過程的應用,但是內容相對分散和零碎,我們有必要針對這個主題作一個歸納性的介紹。采用依賴注入的服務均由某個ServiceProvider來提供,但是在ASP.NET Core管道涉及到兩個不同的ServiceProvider,其中一個是在管道成功構建後創建並綁定到WebHost上的ServiceProvider,對應著WebHost的Services屬性。另一個ServiceProvider則是在管道處理每個請求時即時創建的,它綁定當表示當前請求上下文上,對應著HttpContext的RequestServices屬性,兩個ServiceProvider之間存在著父子關系。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、WebHost的ServiceProvider
二、HttpContext的ServiceProvider
原理分析
實例證明
兩個ServiceProvider具有“父子”關系
ServiceProvidersFeature特性
RequestServicesContainerMiddleware中間件
AutoRequestServicesStartupFilter
ASP.NET Core的依賴注入框架其實很簡單,其中僅僅涉及ServiceCollection和ServiceProvider這兩個核心對象。我們預先將服務描述信息注冊到ServiceCollection之上,然後利用ServiceCollection來創建ServiceProvider,並最終利用後者根據指定的服務類型來提供對應的服務實例。接下來我們以這兩個對象作為唯一的關注點來回顧一下管道的創建流程。ASP.NET Core管道的創建也僅僅涉及到兩個核心對象,作為應用宿主的WebHost對象和創建它的WebHostBuilder。下圖基本揭示了WebHostBuilder創建WebHost,以及WebHost在開啟過程針對依賴注入這兩個核心對象的使用。
ASP.NET Core管道在構建過程中會使用同一個ServiceCollection,所有注冊的服務都被添加到這個對象上。這個ServiceCollection對象最初由WebHostBuilder創建。在WebHost的創建過程中,WebHostBuilder需要向這個ServiceCollection對象注冊兩種類型的服務:一種是確保管道能夠被成功構建並順利處理請求所必需的服務,我們不妨將它們稱為系統服務;另一種則是用戶通過調用ConfigureServices方法自行注冊的服務,我們姑且稱它們為用戶服務。
當上述這兩種服務被成功注冊之後,WebHostBuilder會利用這個ServiceCollection創建一個ServiceProvider對象,這個對象和ServiceCollection將一並遞交給由它創建的WebHost對象。當WebHost在初始化過程中,它的第一項過程就是利用ServiceProvider獲取一個Startup對象。如果這一個ConventionBasedStartup對象是,並且對應的啟動類是一個實例類,具體的啟動對象是采用依賴注入的形式被實例化的,所以啟動類的構造函數是可以有參數的。啟動對象實例化過程中使用的就是WebHostBuilder提供的這個ServiceProvider,這也是依賴注入的第一次應用。
當WebHost利用WebHostBuilder提供的這個ServiceProvider得到這個Startup對象之後,它會調用其ConfigureServices方法將用戶在啟動類中注冊的服務添加到上述這個ServiceCollection對象之上,到目前為止這個ServiceCollection包含了所有需要注冊的服務。如果啟動類型的ConfigureServices方法沒有返回值,那麼這個ServiceCollection將被用來創建一個新的ServiceProvider,後續過程中所有的服務都會利用它來獲取。如果啟動類型的ConfigureServices方法返回一個ServiceProvider,那麼後續過程作為服務提供者的就是這麼一個對象。WebHost的Services屬性返回的就是這個ServiceProvider對象,所以姑且稱它為WebHost的ServiceProvider。
接下來WebHost利用這個ServiceProvider獲取注冊的ApplicationBuilder對象和StartupFilter對象,並將前者作為參數依次調用每個StartupFilter的Configure方法進行中間件的注冊。當針對所有StartupFilter的調用都結束之後,WebHost才會選擇調用Startup對象的Configure方法。對於通過這兩種形式注冊的中間件,如果對應的是一個遵循約定的中間件類型的話,WebHost同樣會采用依賴注入的方式來實例化中間件對象,所以中間件類型的構造函數也是可以有參數的,這是對依賴注入的第二次應用。
到所有中間件都被注冊之後,WebHost會調用ApplicationBuilder的Build方法生成一個RequestDelegate對象,這個對象體現了所有中間件組成一個有序鏈表。接下來,WebHost利用這個RequestDelegate對象創建一個HttpApplication對象(默認創建的是一個HostingHttpApplication對象)。隨後,WebHost利用ServiceProvider提取出最初注冊在WebHostBuilder上的服務器,並將HttpApplication對象作為參數調用其Start方法啟動該服務器。從此,這個以服務器和注冊中間件構成的管道被成功創建出來,服務器隨之開始綁定到指定的監聽地址監聽來自網絡的請求。
請求一旦抵達並被服務器接收,服務器會將它將給後邊的中間件執行。如果中間件對應的是一個按照約定對應的中間件類型,對請求的處理體現在對它的Invoke方法的執行。針對中間件類型Invoke方法的執行同樣采用了依賴注入的形式來提供該方法從第二開始的所有參數,這是對依賴注入的第三次應用。那麼現在問題來了,針對每次請求所使用的ServiceProvider依然是WebHost的ServiceProvider嗎?如果不是 ,那麼兩者是什麼關系?
我們先來回答第一個問題。對於某個由ServiceProvider提供的服務對象說,針對它的回收也是由這個ServiceProvider來完成的。具體來說,非根ServiceProvider在自身被回收的時候,由它提供的采用Scoped和Transient模式的服務實例會自動被回收;至於采用Singleton模式的服務實例,針對它們的回收發生在跟ServiceProvider自身被回收的時候。
如果我們在這個ServiceProvider上以Transient模式注冊了一個服務,這意味著每次從ServiceProvider提取的都是一個全新的對象。如果這些對象引用著一些需要被回收的資源,我們希望資源的回收應該在每次請求處理結束之後自動執行。如果管道每次處理請求時所使用的都是同一個ServiceProvider對象,那麼針對服務實例的回收只能在整個應用終止的時候才會發生,這無疑會產生內存洩漏的問題。基於這個原因。管道總是會創建一個新的ServiceProvider來提供處理每個請求所需的服務,並且這個ServiceProvider將在每次請求處理完成之後被自動回收掉。這樣一個ServiceProvider被創建之後直接保存到當前的HTTP上下文中,我們可以利用HttpContext如下所示的RequestServices屬性得到這個ServiceProvider。
1: public abstract class HttpContext
2: {
3: public abstract IServiceProvider RequestServices { get; set; }
4: ...
5: }
我們上面僅僅從理論層面解釋了為什麼針對每次請求所使用的ServiceProvider都不相同,接下來我們可以通過實例演示的方式來證實這個推論是成立的。我們在一個控制台應用中編寫了如下的代碼來啟動一個ASP.NET Core應用。我們以不同的生命周期模式(Singleton、Scoped和Transient)之注冊三個服務,具體的服務類型都實現了IDisposable接口,而實現的Dispose方法會在控制台上打印相應的文字指示那個類型的Dispose方法被執行了。通過調用Configure方法注冊的中間件會利用從當前HttpContext獲取的ServiceProvider來提供三個對象的服務對象。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .ConfigureLogging(loggerFactory=>loggerFactory.AddConsole())
7: .UseKestrel()
8: .ConfigureServices(svcs=>svcs
9: .AddSingleton<IFoo, Foo>()
10: .AddScoped<IBar, Bar>()
11: .AddTransient<IBaz, Baz>())
12: .Configure(app => app.Run(async context =>{
13: context.RequestServices.GetService<IFoo>();
14: context.RequestServices.GetService<IBar>();
15: context.RequestServices.GetService<IBaz>();
16: await context.Response.WriteAsync("End");
17: }))
18: .Build()
19: .Run();
20: }
21: }
22:
23: public interface IFoo {}
24: public interface IBar {}
25: public interface IBaz {}
26: public class ServiceBase : IDisposable
27: {
28: public void Dispose()
29: {
30: Console.WriteLine($"{this.GetType().Name}.Dispose()...");
31: }
32: }
33: public class Foo : ServiceBase, IFoo {}
34: public class Bar : ServiceBase, IBar {}
35: public class Baz : ServiceBase, IBaz {}
由於我們調用 WebHostBuilder的ConfigureLogging方法添加了ConsoleLoggerProvider,所以管道在開始和結束請求的時候會在當前控制台上寫入相應的日志。啟動應用之後,我們利用浏覽器向默認的監聽地址連續發送兩次請求後,控制台上將會產生如下所示的輸出結果。這樣的輸出結果表明:對於當前請求處理過程中獲取的非Sington服務對象都會請求處理結束之後被自動回收。
1: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
2: Request starting HTTP/1.1 GET http://localhost:5000/
3: Baz.Dispose()...
4: Bar.Dispose()...
5: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
6: Request finished in 74.9439ms 200
7:
8: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
9: Request starting HTTP/1.1 GET http://localhost:5000/
10: Baz.Dispose()...
11: Bar.Dispose()...
12: info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
13: Request finished in 0.8272ms 200
回到前面提到的第二個問題,處理每個請求創建的ServiceProvider和管道構建成功時創建的ServiceProvider(對應WebHost的Services屬性)之間具有怎樣的關系,其實兩者之間的關系很簡單,是“父子”關系。下圖不僅僅體現了這兩種類型的ServiceProvider各自具有的生命周期,同時也體現了它們之間的關系。WebHost的生命周期也就是整個應用的生命周期,所以WebHost的Services屬性返回的ServiceProvider是一個全局單例對象。當WebHost隨著其Dispose方法被調用而被關閉時,它會調用ServiceProvider的Dispose方法。
ASP.NET Core管道針對每個請求的處理都在一個全新的HTTP上下文(HttpContext)中進行,提供請求處理所需服務的ServiceProvider與當前上下文綁定在一起,通過HttpContext對象的RequestServices屬性返回。由於這個ServiceProvider將WebHost的ServiceProvider作為“父親” ,所以之前添加的所有服務注冊對於它來說依然有效。當前請求一旦結束,當前HttpContext自然 “壽終正寢” ,與之關聯的ServiceProvider也隨之被回收釋放。
在了解了兩種類型的ServiceProvider各種具有的生命周期和相互關系之後,我們需要了解這個為請求處理提供服務的ServiceProvider是如何被創建,又是如何被回收釋放的。對作為默認HttpContext的DefaultHttpContext對象來說,它的RequestServices屬性返回的ServiceProvider來源於一個名為ServiceProvidersFeature的特性。所謂的ServiceProvidersFeature特性是對所有實現了IServiceProvidersFeature接口的類型以及對應對象的統稱。如下面的代碼片段所示,這個接口具有一個唯一屬性RequestServices正好用於返回和設置這個ServiceProvider。
1: public interface IServiceProvidersFeature
2: {
3: IServiceProvider RequestServices { get; set; }
4: }
ASP.NET Core默認使用的ServiceProvidersFeature是一個類型為RequestServicesFeature的對象,如下所示的代碼片段體現了它提供ServiceProvider的邏輯。在創建一個RequestServicesFeature對象的時候,我們需要提供一個根據某個ServiceProvider創建 ServiceScopeFactory對象,它所提供的ServiceProvider就是根據這個ServiceScopeFactory提供的ServiceScope對象創建的。我們根據根據提供的代碼可知針對這個屬性的多次調用返回的實際上是同一個ServiceProvider。RequestServicesFeature還是實現IDisposable接口,並在實現的Dispose放過中釋放了這個ServiceScope,我們知道此舉實際上是為了實現對提供的這個ServiceProvider實施回收。
1: public class RequestServicesFeature : IServiceProvidersFeature, IDisposable
2: {
3: private IServiceScopeFactory _scopeFactory;
4: private IServiceProvider _requestServices;
5: private IServiceScope _scope;
6: private bool _requestServicesSet;
7:
8: public RequestServicesFeature(IServiceScopeFactory scopeFactory)
9: {
10: _scopeFactory = scopeFactory;
11: }
12:
13: public IServiceProvider RequestServices
14: {
15: get
16: {
17: if (!_requestServicesSet)
18: {
19: _scope = _scopeFactory.CreateScope();
20: _requestServices = _scope.ServiceProvider;
21: _requestServicesSet = true;
22: }
23: return _requestServices;
24: }
25:
26: set
27: {
28: _requestServices = value;
29: _requestServicesSet = true;
30: }
31: }
32:
33: public void Dispose()
34: {
35: _scope?.Dispose();
36: _scope = null;
37: _requestServices = null;
38: }
39: }
那麼這個RequestServicesFeature特性又是如何被添加到當前HttpContext的特性集合中的呢?這實際上又涉及到一個名為RequestServicesContainerMiddleware的中間件。我們在創建這個中間件的時候需要提供一個ServiceScopeFactory,該中間件會在Invoke方法被執行的時候根據它創建一個RequestServicesFeature對象,並將其添加到當前HttpContext的特性集合中。當後續的請求處理結束之後,添加的這個RequestServicesFeature對象會被回收釋放,並從HttpContext的特性集合中去除。實際上HttpContext的RequestServices返回的ServiceProvider就是在這裡被回收釋放的。
1: public class RequestServicesContainerMiddleware
2: {
3: private readonly RequestDelegate _next;
4: private IServiceScopeFactory _scopeFactory;
5:
6: public RequestServicesContainerMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory)
7: {
8: _scopeFactory = scopeFactory;
9: _next = next;
10: }
11:
12: public async Task Invoke(HttpContext httpContext)
13: {
14:
15: var existingFeature = httpContext.Features.Get<IServiceProvidersFeature>();
16: if (existingFeature?.RequestServices != null)
17: {
18: await _next.Invoke(httpContext);
19: return;
20: }
21:
22: using (var feature = new RequestServicesFeature(_scopeFactory))
23: {
24: try
25: {
26: httpContext.Features.Set<IServiceProvidersFeature>(feature);
27: await _next.Invoke(httpContext);
28: }
29: finally
30: {
31: httpContext.Features.Set(existingFeature);
32: }
33: }
34: }
35: }
RequestServicesContainerMiddleware中間件的注冊最終通過一個StartupFilter對象來完成的,它的類型就是具有如下定義的AutoRequestServicesStartupFilter。對於其Configure方法返回的這個Action<IApplicationBuilder>對象來說,它在注冊這個中間件的時候並沒有明確之定義一個具體的ServiceScopeFactory對象,那麼毫無疑問該中間件使用的ServiceScopeFactory就是根據WebHost的ServiceProvider提供的。WebHost的ServiceProvider提供了一個ServiceScopeFactory,而HttpContext的ServiceProvider又是根據這個ServiceScopeFactory提供的ServiceScope創建的,這兩個ServiceProvider之間的父子關系就是采用形式確立的。
1: public class AutoRequestServicesStartupFilter : IStartupFilter
2: {
3: public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
4: {
5: return app =>
6: {
7: app.UseMiddleware<RequestServicesContainerMiddleware>();
8: next(app);
9: };
10: }
11: }
在WebHostBuilder創建WebHost之前,它會注冊一系列確保後續的管道能夠正常構建並處理請求所必須的服務,這其中就包括這個AutoRequestServicesStartupFilter。綜上所述,通過HttpContext的RequestServices屬性返回的一個用於提供請求處理過程所需服務的ServiceProvider,這個ServiceProvider的創建和回收釋放按是通過一個特性(RequestServicesFeature)、一個中間件(RequestServicesContainerMiddleware)和一個StartupFilter(AutoRequestServicesStartupFilter)相互協作完成的。
我們知道注冊服務具有三種生命周期模式(Singleton、Scoped和Transient)。由於為請求處理提供所需服務的ServiceProvider是基於當前請求上下文的,所以這三種生命周期模式在ASP.NET Core應用中體現了服務實例的復用等級。具體來說,Singleton服務在整個應用生命周期中復用,Scoped服務僅在當前請求上下文中復用,而Transient服務則不能被復用,