閱讀323 返回首頁    go 阿裏雲 go 技術社區[雲棲]


ASP.NET Core中的依賴注入(3): 服務的注冊與提供

在采用了依賴注入的應用中,我們總是直接利用DI容器直接獲取所需的服務實例,換句話說,DI容器起到了一個服務提供者的角色,它能夠根據我們提供的服務描述信息提供一個可用的服務對象。ASP.NET Core中的DI容器體現為一個實現了IServiceProvider接口的對象。

ServiceProvider與ServiceDescriptor
服務的注冊與提供
    利用ServiceProvider來提供服務
    提供一個服務實例的集合
    獲取ServiceProvider自身對象
    對泛型的支持

我一直覺得優秀的設計首先應該是簡單的設計,至少是看起來簡單的設計,這就是我們所謂的大道至簡。作為一個服務的提供者,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: }

3-10對於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: }

接下來采用實例演示的方式來介紹如何利用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>分別獲得針對四個接口的服務實例對象並將類型名稱其輸出到控製台上。運行該程序之後,我們會在控製台上得到如下的輸出結果,由此印證ServiceProvider為我們提供了我們期望的服務實例。

   1: serviceProvider.GetService<IFoo>(): Foo
   2: serviceProvider.GetService<IBar>(): Bar
   3: serviceProvider.GetService<IBaz>(): Baz
   4: serviceProvider.GetService<IGux>(): Gux

如果我們在調用GetService方法的時候將服務類型指定為IEnumerable<T>,那麼返回的結果將會是一個集合對象。除此之外, 我們可以直接調用IServiceProvider如下兩個擴展方法GetServeces達到相同的目的。在這種情況下,ServiceProvider將會利用所有與指定服務類型相匹配的ServiceDescriptor來提供具體的服務實例,這些均會作為返回的集合對象的元素。如果所有的ServiceDescriptor均與指定的服務類型不匹配,那麼最終返回的是一個空的集合對象。

   1: public static class ServiceProviderExtensions
   2: {
   3:     public static IEnumerable<T> GetServices<T>(this IServiceProvider provider);
   4:     public static IEnumerable<object> GetServices(this IServiceProvider provider, Type serviceType);
   5: }

值得一提的是,如果ServiceProvider所在的ServiceCollection包含多個具有相同服務類型(對應ServiceType屬性)的ServiceDescriptor,當我們調用GetService方法獲取單個服務實例的時候,隻有最後一個ServiceDescriptor才是有效的,至於其他的ServiceDescriptor,它們隻有在獲取服務集合的場景下才有意義。

我們通過一個簡單的實例來演示如何利用ServiceProvider得到一個包含多個服務實例的集合。我們在一個控製台應用中定義了如下一個服務接口IFoobar,兩個服務類型Foo和Bar均實現了這個接口。在作為程序入口的Main方法中,我們將針針對服務類型Foo和Bar的兩個ServiceDescriptor添加到創建的ServiceCollection對象中,這兩個ServiceDescriptor對象的ServiceType屬性均為IFoobar。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceCollection serviceCollection = new ServiceCollection()
   6:              .AddSingleton<IFoobar, Foo>()
   7:              .AddSingleton<IFoobar, Bar>();
   8:  
   9:         IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
  10:         Console.WriteLine("serviceProvider.GetService<IFoobar>(): {0}", serviceProvider.GetService<IFoobar>());
  11:  
  12:         IEnumerable<IFoobar> services = serviceProvider.GetServices<IFoobar>();
  13:         int index = 1;
  14:         Console.WriteLine("serviceProvider.GetServices<IFoobar>():");
  15:         foreach (IFoobar foobar in services)
  16:         {
  17:             Console.WriteLine("{0}: {1}", index++, foobar);
  18:         }
  19:     }
  20: }
  21:  
  22: public interface IFoobar {}
  23: public class Foo : IFoobar {}
  24: public class Bar : IFoobar {}

在調用ServiceCollection對象的擴展方法BuildServiceProvider得到對應的ServiceProvider對象之後,我們先調用其GetService<T>方法以確定針對服務接口IFoobar得到的服務實例的真實類型就是是Foo還是Bar。接下來我們調用ServiceProvider的擴展方法GetServices<T>獲取一組針對服務接口IFoobar的服務實例並將它們的真是類型打印在控製台上。該程序運行後將會在控製台上生成如下的輸出結果。

   1: serviceProvider.GetService<IFoobar>(): Bar
   2: serviceProvider.GetServices<IFoobar>():
   3: 1: Foo
   4: 2: Bar

對於ServiceProvider的服務提供機製來說,還有一個小小的細節值得我們關注,那就是當我們調用GetService或者GetRequiredService方法的時候若將服務類型設定為IServiceProvider,那麼得到的對象實際上就是ServiceProvider自身這個對象。與之同理,調用GetServices方法將會返回一個包含自身的集合。如下所示的代碼片段體現了ServiceProvider的這個特性。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider serviceProvider = new ServiceCollection().BuildServiceProvider();
   6:         Debug.Assert(object.ReferenceEquals(serviceProvider, serviceProvider.GetService<IServiceProvider>()));
   7:         Debug.Assert(object.ReferenceEquals(serviceProvider, serviceProvider.GetServices<IServiceProvider>().Single()));
   8:     }
   9: }

ServiceProvider提供的服務實例不僅限於普通的類型,它對泛型服務類型同樣支持。在針對泛型服務進行注冊的時候,我們可以將服務類型設定為攜帶具體泛型參數的“關閉泛型類型”(比如IFoobar<IFoo,IBar>),除此之外服務類型也可以是包含具體泛型參數的“開放泛型類型”(比如IFoo<,>)。前者實際上還是將其視為非泛型服務來對待,後者才真正體現了“泛型”的本質。

比如我們注冊了某個泛型服務接口IFoobar<,>與它的實現類Foobar<,>之間的映射關係,當我們指定一個攜帶具體泛型參數的服務接口類型IFoobar<IFoo,IBar>並調用ServiceProvider的GetService方法獲取對應的服務實例時,ServiceProvider會針對指定的泛型參數類型(IFoo和IBar)來解析與之匹配的實現類型(可能是Foo和Baz)並得到最終的實現類型(Foobar<Foo,Baz>)。

我們同樣利用一個簡單的控製台應用來演示基於泛型的服務注冊與提供方式。如下麵的代碼片段所示,我們定義了三個服務接口(IFoo、IBar和IFoobar<T1,T2>)和實現它們的三個服務類(Foo、Bar個Foobar<T1,T2>),泛型接口具有兩個泛型參數類型的屬性(Foo和Bar),它們在實現類中以構造器注入的方式被初始化。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider serviceProvider = new ServiceCollection()
   6:             .AddTransient<IFoo, Foo>()
   7:             .AddTransient<IBar, Bar>()
   8:             .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>))
   9:             .BuildServiceProvider();
  10:  
  11:         Console.WriteLine("serviceProvider.GetService<IFoobar<IFoo, IBar>>().Foo: {0}", serviceProvider.GetService<IFoobar<IFoo, IBar>>().Foo);
  12:         Console.WriteLine("serviceProvider.GetService<IFoobar<IFoo, IBar>>().Bar: {0}", serviceProvider.GetService<IFoobar<IFoo, IBar>>().Bar);
  13:     }
  14: }
  15:  
  16: public interface IFoobar<T1, T2>
  17: {
  18:     T1 Foo { get; }
  19:     T2 Bar { get; }
  20: }
  21: public interface IFoo {}
  22: public interface IBar {}
  23:  
  24: public class Foobar<T1, T2> : IFoobar<T1, T2>
  25: {
  26:     public T1 Foo { get; private set; }
  27:     public T2 Bar { get; private set; }
  28:     public Foobar(T1 foo, T2 bar)
  29:     {
  30:         this.Foo = foo;
  31:         this.Bar = bar;
  32:     }
  33: }
  34: public class Foo : IFoo {}
  35: public class Bar : IBar {}

在作為入口程序的Main方法中,我們創建了一個ServiceCollection對象並采用Transient模式注冊了上述三個服務接口與對應實現類型之間的映射關係,對於泛型服務IFoobar<T1,T2>/Foobar<T1,T2>來說,我們指定的是不攜帶具體泛型參數的開放泛型類型IFoobar<,>/Foobar<,>。利用此ServiceCollection創建出對應的ServiceProvider之後,我們調用後者的GetService方法並指定IFoobar<IFoo,IBar>為服務類型。得到的服務對象將會是一個Foobar<Foo,Bar>對象,我們將它的Foo和Bar屬性類型輸出於控製台上作為驗證。該程序執行之後將會在控製台上產生下所示的輸出結果。

   1: serviceProvider.GetService<IFoobar<IFoo, IBar>>().Foo: Foo 
   2: serviceProvider.GetService<IFoobar<IFoo, IBar>>().Bar: Bar 

ASP.NET Core中的依賴注入(1):控製反轉(IoC)
ASP.NET Core中的依賴注入(2):依賴注入(DI)
ASP.NET Core中的依賴注入(3):服務注冊與提取
ASP.NET Core中的依賴注入(4):構造函數的選擇與生命周期管理
ASP.NET Core中的依賴注入(5):ServicePrvider實現揭秘【總體設計】
ASP.NET Core中的依賴注入(5):ServicePrvider實現揭秘【解讀ServiceCallSite】
ASP.NET Core中的依賴注入(5):ServicePrvider實現揭秘【補充漏掉的細節】


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 14:34:57

  上一篇:go  ASP.NET Core中的依賴注入(2):依賴注入(DI)
  下一篇:go  ASP.NET Core中的依賴注入(4): 構造函數的選擇與服務生命周期管理