毫不誇張地說,整個ASP.NET Core就是建立在依賴注入框架之上的。ASP.NET Core應用在啟動時構建管道所需的服務,以及管道處理請求使用到的服務,均來源於依賴注入容器。依賴注入容器不僅為ASP.NET Core框架自身提供必要的服務,還為應用程序提供服務,依賴注入已經成為ASP.NET Core應用的基本編程模式。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[301]普通服務的注冊和提取(源代碼)
[302]針對泛型服務類型的支持(源代碼)
[303]為同一類型提供多個服務注冊(源代碼)
[304]服務實例的生命周期(源代碼)
[305]服務實例的釋放回收(源代碼)
[306]服務范圍的驗證(源代碼)
[307]服務注冊有效性的驗證(源代碼)
[301]普通服務的注冊和提取
我們提供的演示實例是一個控制台程序。在添加了“Microsoft.Extensions.DependencyInjection”NuGet包引用之后,我們定義了如下接口和實現類型來表示相應的服務。如代碼片段所示,Foo、Bar和Baz分別實現了對應的接口IFoo、IBar與IBaz。它們派生的基類Base實現了IDisposable接口,我們在其構造函數和實現的Dispose方法中打印出相應的文字以確定服務實例被創建和釋放的時機。我們還定義了泛型的接口IFoobar<T1, T2>和對應的實現類Foobar<T1, T2>,后面講用它們來演示針對泛型服務實例的提供。
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IFoobar<T1, T2> {} public class Base : IDisposable { public Base() => Console.WriteLine($"An instance of {GetType().Name} is created."); public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed."); } public class Foo : Base, IFoo, IDisposable { } public class Bar : Base, IBar, IDisposable { } public class Baz : Base, IBaz, IDisposable { } public class Foobar<T1, T2>: IFoobar<T1,T2> { public T1 Foo { get; } public T2 Bar { get; } public Foobar(T1 foo, T2 bar) { Foo = foo; Bar = bar; } }
在如下所示的演示程序中,我們創建了一個ServiceCollection對象(ServiceCollection實現了IServiceCollection接口),並現有調用AddTransient、AddScoped和AddSingleton擴展方法針對IFoo、IBar和IBaz接口注冊了對應的服務,從方法命名可以看出注冊的服務采用的生命周期模式分別為Transient、Scoped和Singleton。我們接下來調用IServiceCollection對象的BuildServiceProvider擴展方法創建出代表依賴注入容器的IServiceProvider對象,並調用它的GetService<T>擴展方法來提供所需的服務實例。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; var provider = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar>(_ => new Bar()) .AddSingleton<IBaz, Baz>() .BuildServiceProvider(); Debug.Assert(provider.GetService<IFoo>() is Foo); Debug.Assert(provider.GetService<IBar>() is Bar); Debug.Assert(provider.GetService<IBaz>() is Baz);
[302]針對泛型服務類型的支持
表示依賴注入容器的IServiceProvider對象還能提供泛型服務實例。如下面的代碼片段所示,在為創建的ServiceCollection對象添加了針對IFoo和IBar接口的服務注冊之后,我們調用AddTransient方法注冊了針對泛型定義IFoobar<,>的服務(實現的類型為Foobar<,>)。在構建出代表依賴注入容器的IServiceProvider對象之后,我們利用它提供一個類型為IFoobar<IFoo, IBar>的服務實例(S302)。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; var provider = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) .BuildServiceProvider(); var foobar = (Foobar<IFoo, IBar>?)provider.GetService<IFoobar<IFoo, IBar>>(); Debug.Assert(foobar?.Foo is Foo); Debug.Assert(foobar?.Bar is Bar);
[303]為同一類型提供多個服務注冊
我們可以為同一個類型添加多個服務注冊,雖然所有服務注冊均是有效的,但是GetService<T>擴展方法只能返回一個服務實例。框架采用了“后來居上”的策略,總是采用最近添加的服務注冊來創建服務實例。GetServices<TService>擴展方法將利用指定服務類型的所有服務注冊來提供一組服務實例。需要的演示程序添加了三個針對Base類型的服務注冊,對應的實現類型分別為Foo、Bar和Baz。我們將Base作為泛型參數調用了GetServices<Base>方法,返回的集合將包含這三個類型的對象。
using App; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; using System.Linq; var services = new ServiceCollection() .AddTransient<Base, Foo>() .AddTransient<Base, Bar>() .AddTransient<Base, Baz>() .BuildServiceProvider() .GetServices<Base>(); Debug.Assert(services.OfType<Foo>().Any()); Debug.Assert(services.OfType<Bar>().Any()); Debug.Assert(services.OfType<Baz>().Any());
[304]服務實例的生命周期
代表依賴注入容器的IServiceProvider對象之間的層次結構促成了服務實例的三種生命周期模式。具體來說,由於Singleton服務實例保存在作為根容器的IServiceProvider對象上,所以能夠在多個同根IServiceProvider對象之間提供真正的單例保證。Scoped服務實例被保存在當前服務范圍對應的IServiceProvider對象上,所以只能在當前服務范圍內保證提供的實例是單例的。對應類型沒有實現IDisposable接口的Transient服務實例則采用“即用即建,用后即棄”的策略。
我們接下來演示三種不同生命周期模式的差異。如下面代碼片段所示,我們創建了一個ServiceCollection對象,並針對接口IFoo、IBar和IBaz注冊了對應的服務,采用的生命周期模式分別為Transient、Scoped和Singleton。IServiceProvider對象被構建出來后,我們調用其CreateScope方法創建了兩個代表“服務范圍”的IServiceScope對象,它的ServiceProvider屬性提供所在服務范圍的IServiceProvider對象,實際上是當前IServiceProvider對象的子容器。我們最后利用作為子容器的這個IServiceProvider對象來提供所需的服務實例。
using App; using Microsoft.Extensions.DependencyInjection; var root = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar>(_ => new Bar()) .AddSingleton<IBaz, Baz>() .BuildServiceProvider(); var provider1 = root.CreateScope().ServiceProvider; var provider2 = root.CreateScope().ServiceProvider; GetServices<IFoo>(provider1); GetServices<IBar>(provider1); GetServices<IBaz>(provider1); Console.WriteLine(); GetServices<IFoo>(provider2); GetServices<IBar>(provider2); GetServices<IBaz>(provider2); static void GetServices<T>(IServiceProvider provider) { provider.GetService<T>(); provider.GetService<T>(); }
演示程序啟動后會在控制台上輸出如圖1所示的結果。由於IFoo服務被注冊為Transient服務,所以四次服務獲取請求都會創建一個新的Foo對象。IBar服務的生命周期模式為Scoped,同一個IServiceProvider對象只會創建一個Bar對象,所以整個過程中會創建兩個Bar對象。IBaz服務采用Singleton生命周期,具有同根的兩個IServiceProvider對象提供的是同一個Baz對象。
圖1 IServiceProvider對象按照服務注冊對應的生命周期模式提供服務實例
[305]服務實例的釋放回收
作為依賴注入容器的IServiceProvider對象不僅用來構建並提供服務實例,還負責管理這服務實例的生命周期。如果某個服務實例的類型實現了IDisposable接口,就意味着當生命周期完結的時候需要調用Dispose方法執行一些資源釋放操作,針對服務實例的釋放同樣由IServiceProvider對象來負責。框架針對提供服務實例的釋放策略取決於采用的生命周期模式,具體的策略如下。
- Transient和Scoped:所有實現了IDisposable接口的服務實例會被當前IServiceProvider對象保存起來,當IServiceProvider對象的Dispose方法被調用的時候,這些服務實例的Dispose方法會隨之被調用。
- Singleton:服務實例保存在作為根容器的IServiceProvider對象上,只有當后者的Dispose方法被調用的時候,這些服務實例的Dispose方法才會隨之被調用。
ASP.NET Core應用具有一個代表根容器的IServiceProvider對象,由於它與應用具有一致的生命周期而被稱為ApplicationServices。對於處理的每一次請求,應用都會利用這個根容器來創建基於當前請求的服務范圍,該服務范圍所在的IServiceProvider對象被稱為RequestServices,處理請求所需的服務實例均由它來提供。請求處理完成之后,創建的服務范圍被終結,RequestServices也隨之被釋放,此時在當前請求范圍內創建的Scoped服務實例和實現了IDisposable接口的Transient服務實例得以及時釋放。
上述釋放策略可以通過如下演示實例進行印證。如代碼片段所示,我們並采用不同的生命周期模式添加了針對IFoo、IBar和IBaz的服務注冊。在作為根容器的IServiceProvider對象被構建出來后,可以調用其CreateScope方法創建出對應的服務范圍。我們利用服務范圍所在的IServiceProvider對象提供了三個對應的實例。
using App; using Microsoft.Extensions.DependencyInjection; using (var root = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar, Bar>() .AddSingleton<IBaz, Baz>() .BuildServiceProvider()) { using (var scope = root.CreateScope()) { var provider = scope.ServiceProvider; provider.GetService<IFoo>(); provider.GetService<IBar>(); provider.GetService<IBaz>(); Console.WriteLine("Child container is disposed."); } Console.WriteLine("Root container is disposed."); }
由於代表根容器的IServiceProvider對象和服務范圍的創建都是在using塊中進行的,所以所有針對它們的Dispose方法都會在using塊結束的地方被調用。該程序運行之后在控制台上輸出的結果如圖2所示,可以看到當作為子容器的IServiceProvider對象被釋放的時候,由它提供的兩個生命周期模式分別為Transient和Scoped的服務實例(Foo和Bar)被正常釋放。對於生命周期模式為Singleton的服務實例Baz來說,它的Dispose方法會延遲到作為根容器的IServiceProvider對象被釋放的時候才執行。
[306]服務范圍的驗證
Singleton和Scoped這兩種不同的生命周期是通過將提供的服務實例分別存放到作為根容器的IServiceProvider對象和當前IServiceProvider對象來實現的,這意味着作為根容器的IServiceProvider對象提供的Scoped服務實例也是單例的。如果某個Singleton服務依賴另一個Scoped服務,那么Scoped服務實例將被一個Singleton服務實例所引用,也就意味着Scoped服務實例也成了一個Singleton服務實例。在ASP.NET Core應用中,我們一般只會將於請求具有一致生命周期的服務注冊為Scope模式。一旦出現上述這種情況,就意味着Scoped服務實例將變成一個Singleton服務實例,這基本上不是我們希望看到的結果,這極有可能造成嚴重的內存泄露問題。為了避免這種情況的發生,框架提供了相應的驗證機制。
如果希望IServiceProvider對象在提供服務時針對服務范圍作有效性檢驗,我們只需要在調用IServiceCollection接口的BuildServiceProvider擴展方法時提供一個值為True作為參數即可。下面的演示程序定義了兩個服務接口(IFoo和IBar)和對應的實現類型(Foo和Bar),其中,Foo需要依賴IBar。如果將IFoo和IBar分別注冊為Singleton服務與Scoped服務,當調用BuildServiceProvider方法創建代表依賴注入容器的IServiceProvider對象的時候將validateScopes參數設置為True即可。下面這個實例演示了這種驗證方式。
using App; using Microsoft.Extensions.DependencyInjection; var root = new ServiceCollection() .AddSingleton<IFoo, Foo>() .AddScoped<IBar, Bar>() .BuildServiceProvider(true); var child = root.CreateScope().ServiceProvider; ResolveService<IFoo>(root); ResolveService<IBar>(root); ResolveService<IFoo>(child); ResolveService<IBar>(child); void ResolveService<T>(IServiceProvider provider) { var isRootContainer = root == provider ? "Yes" : "No"; try { provider.GetService<T>(); Console.WriteLine($"Status: Success; Service Type: { typeof(T).Name}; Root: { isRootContainer}"); } catch (Exception ex) { Console.WriteLine($"Status: Fail; Service Type: { typeof(T).Name}; Root: { isRootContainer}"); Console.WriteLine($"Error: {ex.Message}"); } } public interface IFoo {} public interface IBar {} public class Foo : IFoo { public IBar Bar { get; } public Foo(IBar bar) => Bar = bar; } public class Bar : IBar {}
上面的演示實例啟動之后在控制台上輸出的結果如圖3所示。從輸出結果可以看出,四個服務提取請求只有一次(使用代表子容器的IServiceProvider提供IBar服務實例)是成功的。這個實例充分說明了一旦開啟了針對服務范圍的驗證,IServiceProvider對象不可能提供以單例形式存在的Scoped服務實例。
[307]服務注冊有效性的驗證
針對服務范圍的檢驗體現在ServiceProviderOptions配置選項的ValidateScopes屬性上。如下面的代碼片段所示,ServiceProviderOptions還具有另一個名為ValidateOnBuild的屬性。如果將該屬性設置為True,就意味着IServiceProvider對象被構建的時候會對每個ServiceDescriptor對象實施有效性驗證。
public class ServiceProviderOptions { public bool ValidateScopes { get; set; } public bool ValidateOnBuild { get; set; } }
我們照例來做一個在構建IServiceProvider對象時檢驗服務注冊有效性的例子。如下面的代碼片段所示,我們定義了一個IFoobar接口和對應的實現類型Foobar。由於希望總是希望以單例的形式來使用Foobar對象,我們為了定義了唯一的私有構造函數。
public interface IFoobar {} public class Foobar : IFoobar { private Foobar() {} public static readonly Foobar Instance = new Foobar(); }
我們在演示程序中定義了如下這個BuildServiceProvider方法來完成針對IFoobar/Foobar的服務注冊和最終對IServiceProvider對象的構建。我們在調用BuildServiceProvider擴展方法創建對應IServiceProvider對象時指定了一個ServiceProviderOptions對象,而該對象的ValidateOnBuild屬性來源於內嵌方法的同名參數。
using App; using Microsoft.Extensions.DependencyInjection; BuildServiceProvider(false); BuildServiceProvider(true); static void BuildServiceProvider(bool validateOnBuild) { try { var options = new ServiceProviderOptions { ValidateOnBuild = validateOnBuild }; new ServiceCollection() .AddSingleton<IFoobar, Foobar>() .BuildServiceProvider(options); Console.WriteLine($"Status: Success; ValidateOnBuild: {validateOnBuild}"); } catch (Exception ex) { Console.WriteLine($"Status: Fail; ValidateOnBuild: {validateOnBuild}"); Console.WriteLine($"Error: {ex.Message}"); } }
由於Foobar具有唯一的私有構造函數,而提供的服務注冊並不能將服務實例創建出來,所以這個服務注冊是無效的。由於在默認情況下構建IServiceProvider對象的時候並不會對服務注冊做有效性檢驗,所以此時無效的服務注冊並不會及時被探測到。一旦將ValidateOnBuild選項設置為True,IServiceProvider對象在被構建的時候就會拋出異常,圖4所示的輸出結果就體現了這一點。