ASP.NET Core中的依賴注入(3): 服務的註冊與提供
在採用了依賴注入的應用中,我們總是直接利用DI容器直接獲取所需的服務例項,換句話說,DI容器起到了一個服務提供者的角色,它能夠根據我們提供的服務描述資訊提供一個可用的服務物件。ASP.NET Core中的DI容器體現為一個實現了IServiceProvider介面的物件。
ServiceProvider與ServiceDescriptor
服務的註冊與提供
利用ServiceProvider來提供服務
提供一個服務例項的集合
獲取ServiceProvider自身物件
對泛型的支援
一、ServiceProvider與ServiceDescriptor
我一直覺得優秀的設計首先應該是簡單的設計,至少是看起來簡單的設計,這就是我們所謂的大道至簡。作為一個服務的提供者,ASP.NET Core中的DI容器最終體現為一個IServiceProvider介面,我們將所有實現了該介面的型別及其例項統稱為ServiceProvider。如下面的程式碼片段所示,該介面簡單至極,它僅僅提供了唯一個GetService方法,該方法根據提供的服務型別為你提供對應的服務例項。
1: public interface IServiceProvider
2: {
3: object GetService(Type serviceType);
4: }
ASP.NET Core內部真正使用的是一個實現了IServiceProvider介面的內部型別(該型別的名稱為“ServiceProvider”),我們不能直接建立該物件,只能間接地通過呼叫IServiceCollection介面的擴充套件方法BuildServiceProvider得到它。IServiceCollection介面定義在“Microsoft.Extensions.DependencyInjection”名稱空間下,如果沒有特別說明,本系列文章涉及到的與ASP.NET Core依賴注入相關的型別均採用此名稱空間。 如下面的程式碼片段所示,IServiceCollection介面實際上代表一個元素為ServiceDescriptor物件的集合,它直接繼承了另一個介面IList<ServiceDescriptor>,而ServiceCollection類實現了該介面。
1: public static class ServiceCollectionExtensions
2: {
3: public static IServiceProvider BuildServiceProvider(this IServiceCollection services);
4: }
5:
6: public interface IServiceCollection : IList<ServiceDescriptor>
7: {}
8:
9: Public class ServiceCollection: IServiceCollection
10: {
11: //省略成員
12: }
體現為DI容器的ServiceProvider之所以能夠根據我們給定的服務型別(一般是一個介面型別)提供一個能夠開箱即用的服務例項,是因為我們預先註冊了相應的服務描述資訊,這些指導ServiceProvider正確實施服務提供操作的服務描述體現為如下一個ServiceDescriptor型別。
1: public class ServiceDescriptor
2: {
3: public ServiceDescriptor(Type serviceType, object instance);
4: public ServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime);
5: public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime);
6:
7: public Type ServiceType { get; }
8: public ServiceLifetime Lifetime { get; }
9:
10: public Type ImplementationType { get; }
11: public object ImplementationInstance { get; }
12: public Func<IServiceProvider, object> ImplementationFactory { get; }
13: }
ServiceDescriptor的ServiceType屬性代表提供服務的生命型別,由於標準化的服務一般會定義成介面,所以在絕大部分情況下體現為一個介面型別。型別為ServiceLifetime的屬性Lifetime體現了ServiceProvider針對服務例項生命週期的控制方式。如下面的程式碼片段所示,ServiceLifetime是一個美劇型別,定義其中的三個選項(Singleton、Scoped和Transient)體現三種對服務物件生命週期的控制形式,我們將在本節後續部分對此作專門的介紹。
1: public enum ServiceLifetime
2: {
3: Singleton,
4: Scoped,
5: Transient
6: }
對於ServiceDescriptor的其他三個屬性來說,它們實際上是輔助ServiceProvider完成具體的服務例項提供操。ImplementationType屬性代表被提供服務例項的真實型別,屬性ImplementationInstance則直接代表被提供的服務例項,ImplementationFactory則提供了一個建立服務例項的委託物件。ASP.NET Core與依賴注入相關的幾個核心型別具有如圖10所示的關係。
由於ASP.NET Core中的ServiceProvider是根據一個代表ServiceDescriptor集合的IServiceCollection物件建立的,當我們呼叫其GetService方法的時候,它會根據我們提供的服務型別找到對應的ServiceDecriptor物件。如果該ServiceDecriptor物件的ImplementationInstance屬性返回一個具體的物件,該物件將直接用作被提供的服務例項。如果ServiceDecriptor物件的ImplementationFactory返回一個具體的委託,該委託物件將直接用作建立服務例項的工廠。
如果這兩個屬性均為Null,ServiceProvider才會根據ImplementationType屬性返回的型別呼叫相應的建構函式建立被提供的服務例項。至於我們在上面一節中提到的三種依賴注入方式,ServiceProvider僅僅支援構造器注入,屬性注入和方法注入的支援並未提供。
二、服務的註冊與提供
ASP.NET Core針對依賴注入的程式設計主要體現在兩個方面:其一,建立一個ServiceCollection物件並將服務註冊資訊以ServiceDescriptor物件的形式新增其中;其二,針對ServiceCollection物件建立對應的ServiceProvider並利用它提供我們需要的服務例項。
在進行服務註冊的時候,我們可以直接呼叫相應的建構函式建立ServiceDescriptor物件並將其新增到ServiceCollection物件之中。除此之外,IServiceCollection介面還具有如下三組擴充套件方法將這兩個步驟合二為一。從下面給出的程式碼片段我們不難看出這三組擴充套件方法分別針對上面我們提及的三種針對服務例項的生命週期控制方式,泛型引數TService代表服務的宣告型別,即ServiceDescriptor的ServiceType屬性,至於ServiceDescriptor的其他屬性,則通過方法相應的引數來提供。
1: public static class ServiceCollectionExtensions
2: {
3: public static IServiceCollection AddScoped<TService>(this IServiceCollection services) where TService: class;
4: //其他AddScoped<TService>過載
5:
6: public static IServiceCollection AddSingleton<TService>(this IServiceCollection services) where TService: class;
7: //其他AddSingleton<TService>過載
8:
9: public static IServiceCollection AddTransient<TService>(this IServiceCollection services) where TService: class;
10: //其他AddTransient<TService>過載
11: }
對於用作DI容器的ServiceProvider物件來說,我們可以直接呼叫它的GetService方法根據指定的服務型別獲得想用的服務例項。除此之外,服務的提供還可以通過IServiceProvider介面相應的擴充套件方法來完成。如下面的程式碼片段所示,擴充套件方法GetService<T>以泛型引數的形式指定服務的宣告型別。至於另外兩個擴充套件方法GetRequiredService和GetRequiredService<T>,如果ServiceProvider不能提供一個具體的服務例項,一個InvalidOperationException異常會被丟擲來並提示相應的服務註冊資訊不足。
1: public static class ServiceProviderExtensions
2: {
3: public static T GetService<T>(this IServiceProvider provider);
4: public static object GetRequiredService(this IServiceProvider provider, Type serviceType);
5: public static T GetRequiredService<T>(this IServiceProvider provider);
6: }
利用ServiceProvider來提供服務
接下來採用例項演示的方式來介紹如何利用ServiceCollection進行服務註冊,以及如何利用ServiceCollection建立對應的ServiceProvider來提供我們需要的服務例項。我們建立一個ASP.NET Core控制檯程式,並在project.json中按照如下的方式新增針對 “Microsoft.Extensions.DepedencyInjection”這個NuGet包的依賴。
1: {
2: "dependencies": {
3: "Microsoft.Extensions.DependencyInjection": "1.0.0-rc1-final"
4: },
5: ...
6: }
我們接下來定義四個服務介面(IFoo、IBar、IBaz和IGux)以及分別實現它們的四個服務類(Foo、Bar、Baz和Gux)如下面的程式碼片段所示,IGux具有三個只讀屬性(Foo、Bar和Baz)均為介面型別,並在建構函式中進行初始化。
1: public interface IFoo {}
2: public interface IBar {}
3: public interface IBaz {}
4: public interface IGux
5: {
6: IFoo Foo { get; }
7: IBar Bar { get; }
8: IBaz Baz { get; }
9: }
10:
11: public class Foo : IFoo {}
12: public class Bar : IBar {}
13: public class Baz : IBaz {}
14: public class Gux : IGux
15: {
16: public IFoo Foo { get; private set; }
17: public IBar Bar { get; private set; }
18: public IBaz Baz { get; private set; }
19:
20: public Gux(IFoo foo, IBar bar, IBaz baz)
21: {
22: this.Foo = foo;
23: this.Bar = bar;
24: this.Baz = baz;
25: }
26: }
現在我們在作為程式入口的Main方法中建立了一個ServiceCollection物件,並採用不同的方式完成了針對四個服務介面的註冊。具體來說,對於正對服務介面IFoo和IGux的ServiceDescriptor來說,我們指定了代表服務真實型別的ImplementationType屬性,而對於針對服務介面IBar和IBaz的ServiceDescriptor來說,我們初始化的則是分別代表服務例項和服務工廠的ImplementationInstance個ImplementationFactory屬性。由於我們呼叫的是AddSingleton方法,所以四個ServiceDescriptor的Lifetime屬性均為Singleton。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: IServiceCollection services = new ServiceCollection()
6: .AddSingleton<IFoo, Foo>()
7: .AddSingleton<IBar>(new Bar())
8: .AddSingleton<IBaz>(_ => new Baz())
9: .AddSingleton<IGux, Gux>();
10:
11: IServiceProvider serviceProvider = services.BuildServiceProvider();
12: Console.WriteLine("serviceProvider.GetService<IFoo>(): {0}",serviceProvider.GetService<IFoo>());
13: Console.WriteLine("serviceProvider.GetService<IBar>(): {0}", serviceProvider.GetService<IBar>());
14: Console.WriteLine("serviceProvider.GetService<IBaz>(): {0}", serviceProvider.GetService<IBaz>());
15: Console.WriteLine("serviceProvider.GetService<IGux>(): {0}", serviceProvider.GetService<IGux>());
16: }
17: }
接下來我們呼叫ServiceCollection物件的擴充套件方法BuildServiceProvider得到對應的ServiceProvider物件,然後呼叫其擴充套件方法GetService<T>分別獲得針對四個介面的服務例項物件並將型別名稱其輸出到控制檯上。執行該程式之後,