1. 程式人生 > >解析 .Net Core 注入——註冊服務

解析 .Net Core 注入——註冊服務

在學習 Asp.Net Core 的過程中,注入可以說是無處不在,對於 .Net Core 來說,它是獨立的一個程式集,沒有複雜的依賴項和配置檔案,所以對於學習 Asp.Net Core 原始碼的朋友來說,注入作為一個起點非常合適,園子裡確實有許多關於注入的部落格,不過 .Net Core2.0 已經出來了,注入這一塊做了一些 更新,其實有不少 .net 開發人員對 微軟改來改去這一點不是很滿意,加大了學習成本,其實改動分為兩種,一種是 Asp.Net Core Mvc 常用 Api 介面的更改(或者配置的更改),這點在 2.0 以來很少有這樣的情況了,也就是說 Asp.Net Core Mvc 基本趨於穩定了,另一類就是對程式碼的優化,前者對研發的跟進造成了很大的傷害值,而後者對於研發而言無關緊要,對於樂於學習原始碼的程式設計師而言或許能從中帶來許多思考。   所以我打算重新分析 .Net Core2.0 的注入 ,實際釋出版本為 .netstandard2.0 程式集為 Microsoft.Extensions.DependencyInjection.dll。   在 .Net Core 中,注入描述為為三個過程,註冊服務->建立容器->建立物件,所以我也會分為三個模組來介紹
   注入元資料   如果接觸過 .Net Core 則或多或少已經接觸過注入,下面的程式碼註冊了具有三種生命週期的服務,然後建立一個容器,最後使用容器提供這三個服務的例項物件,我們觀察他們的生命週期,看到輸出結果基本對 AddTransient 以及 AddSingleton 這兩種方式註冊的服務具有怎樣的生命週期都會有所判斷,而 AddScoped 方式註冊的服務就複雜一點。   我們看到通過 BuilderServiceProvider 方法建立了一個容器,而容器呼叫 CreateScope 就可以建立了兩個具有範圍的容器,而 AddScoped 方式註冊的服務在不同範圍內的生命週期是不一樣的,而相同範圍下的生命週期和 AddSingleton 是一致的。
  interface ITransient { }   class Transient : ITransient { }   interface ISingleton { }   class Singleton : ISingleton { }   interface IScoped { }   class Scoped : IScoped { }   class Program   {       static void Main(string[] args)       {           IServiceCollection services = new ServiceCollection();           services = services.AddTransient<ITransient, Transient>();           services = services.AddScoped<IScoped, Scoped>();           services = services.AddSingleton<ISingleton, Singleton>();           IServiceProvider serviceProvider = services.BuildServiceProvider();                       Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ITransient>(), serviceProvider.GetService<ITransient>()));           Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IScoped>(), serviceProvider.GetService<IScoped>()));           Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ISingleton>(), serviceProvider.GetService<ISingleton>()));           IServiceProvider serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;           IServiceProvider serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;           Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider1.GetService<IScoped>()));           Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider2.GetService<IScoped>()));           Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<ISingleton>(), serviceProvider2.GetService<ISingleton>()));           /* False            * True            * True            * True            * False            * True            */       }   }
   IServiceCollection
  public interface IServiceCollection : IList<ServiceDescriptor>   {   }
  是一個集合,用來存放使用者註冊的服務元資料    ServiceDescriptor   看上面的例子我們如何添加註入應該也能猜到 ServiceDescriptor 包含哪些屬性了吧!至少包含一個介面型別、實現型別和生命週期,是的就是如此。
  public class ServiceDescriptor   {       public ServiceLifetime Lifetime { get; }       public Type ServiceType { get; }       public Type ImplementationType { get; }       public object ImplementationInstance { get; }       public Func<IServiceProvider, object> ImplementationFactory { get; }   }
  在第一個程式碼塊中,都是使用的是 IServiceCollection 如下簽名拓展方法註冊服務的,這裡我把它稱為“服務型別例項型別”(提供一個服務型別,一個例項型別)的註冊方式,相應的服務型別和例項型別通過解析泛型引數傳遞給 ServiceDescriptor 的ServiceType、ImplementationInstance,值得注意的是,建立 ServiceDescriptor 並不會校驗例項型別的可建立性(驗證其是否是抽象類,介面)
  public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)       where TService : class       where TImplementation : class, TService   {       if (services == null)       {           throw new ArgumentNullException(nameof(services));       }       return services.AddTransient(typeof(TService), typeof(TImplementation));   }
  此外,微軟還提供了“服務例項”(提供一個服務型別,一個例項物件)以及“服務例項工廠”(提供一個服務型別,一個例項物件工廠)的註冊方式,前者只供單例服務使用,使用起來也很簡單   services.AddTransient<ITransient>(_=>new Transient());   services.AddSingleton<ISingleton>(new Singleton());   關於 ServiceDescriptor,還有一個要說的就是服務的生命週期了,使用 AddSingleton、AddScoped、AddTransient 三種方式註冊的服務在 ServiceDescriptor 中的 LifeTime 屬性分別對應下面這個列舉型別
  public enum ServiceLifetime   {       Singleton,       Scoped,       Transient   }
  1、Transient:每次從容器 (IServiceProvider)中獲取的時候都是一個新的例項   2、Singleton:每次從同根容器中(同根 IServiceProvider)獲取的時候都是同一個例項   3、Scoped:每次從同一個容器中獲取的例項是相同的、   關於服務的生命週期,如果還不清楚也沒關係,因為接下來會不斷的學習它    自定義建立容器和建立物件的過程   在 文章的開頭就介紹了該注入框架的三個過程,註冊服務->建立容器->建立物件,然而註冊服務的步驟是非常簡單的,將一個個類似 AddTransient、AddSingleton 的方法提供的泛型引數或者實參轉換成一個 ServiceDescriptor 物件儲存在 IServiceCollection 中,而建立容器和床物件是否也是這樣簡單呢?如果是,想必很容易寫出下面的程式碼
  public class MyServiceProvider : IServiceProvider   {       private List<ServiceDescriptor> serviceDescriptors = new List<ServiceDescriptor>();       private Dictionary<Type, object> SingletonServices = new Dictionary<Type, object>();       public MyServiceProvider(IEnumerable<ServiceDescriptor>  serviceDescriptors)       {           this.serviceDescriptors.AddRange(serviceDescriptors);       }       public object GetService(Type serviceType)       {           var descriptor = serviceDescriptors.FirstOrDefault(t => t.ServiceType == serviceType);           if(descriptor == null)           {               throw new Exception($"服務‘{serviceType.Name}’未註冊");           }           else           {               switch (descriptor.Lifetime)               {                   case ServiceLifetime.Singleton:                       if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))                       {                           return obj;                       }                       else                       {                           var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);                           SingletonServices.Add(descriptor.ServiceType, singletonObject);                           return singletonObject;                       }                   case ServiceLifetime.Scoped:                       throw new NotSupportedException($"建立失敗,暫時不支援 Scoped");                   case ServiceLifetime.Transient:                       var transientObject = Activator.CreateInstance(descriptor.ImplementationType);                       return transientObject;                   default:                       throw new NotSupportedException("建立失敗,不能識別的 LifeTime");               }           }       }   }   public static class ServiceCollectionContainerBuilderExtensions   {public static MyServiceProvider BuildeMyServiceProvider(this IServiceCollection services)       {           return new MyServiceProvider(services);       }   }
  由於 Scoped 的特殊性,部分人寫到這裡就戛然而止了,然而還有一個問題,我們知道註冊服務的時候可能採取多種方式,這裡只給出了"服務例項型別"的情形,稍作修改
  case ServiceLifetime.Singleton:       if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))       {           return obj;       }       else       {           if(descriptor.ImplementationType != null)           {               var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);               SingletonServices.Add(descriptor.ServiceType, singletonObject);               return singletonObject;           }           else if(descriptor.ImplementationInstance != null)           {               SingletonServices.Add(descriptor.ServiceType, descriptor.ImplementationInstance);               return descriptor.ImplementationInstance;           }           else if(descriptor.ImplementationFactory != null)           {               var singletonObject = descriptor.ImplementationFactory.Invoke(this);               SingletonServices.Add(descriptor.ServiceType, singletonObject);               return singletonObject;           }           else           {               throw new Exception("建立服務失敗,無法找到例項型別或例項");           }       }
  雖然這裡只重寫了 Singleton 方式,但是其他的也應如此,實際上可以一直這麼寫下去,但是作為 C# 開發者就顯得有些不優雅,因為這是面向過程(或者說是基於物件)的開開發模式   此外,微軟的注入是不支援屬性注入的,但是別忘了,仍然是支援建構函式注入的,要不然這個注入那也太雞助了吧!是的,按照上述的程式碼段我們可以繼續寫下去,在解析出例項型別的時候,我們找到它的建構函式,找到建構函式的所有引數,以同樣的方式建立引數的例項,這是一個遞迴的過程,最後回撥,仍然可以建立我們需要的物件,但是這一切如何健壯、優雅的實現呢?這就是學習原始碼原因所在吧!    微軟是如何進一步處理元資料的?   其實上面的程式碼最主要的問題就是建立容器和建立物件這兩個過程過度耦合了,並且存在一個最大的問題,仔細想想每次建立物件的時候都要去翻一遍 ServiceDescriptor 判斷它是以“服務例項型別”、“服務例項物件”、“服務例項物件工廠”中的哪種方式註冊的,這樣就進行了一些不必要的效能消耗,然而這個工作微軟是在建立容器的時候完成的。跟隨著建立容器的過程我們義無反顧的向原始碼走去!去哪?尋找微軟和如何處理 ServiceDescriptor 的!   這裡我們遇到的第一個攔路虎就是 ServiceProvider,我們建立的容器最終就是一個這樣的型別,看看它是如何建立物件的?
  public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback   {       private readonly IServiceProviderEngine _engine;       internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)       {           //此處省略了一些程式碼           switch (options.Mode)           {               case ServiceProviderMode.Dynamic:                   _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);                   break;               //此處省略了一些程式碼               default:                   throw new ArgumentOutOfRangeException(nameof(options.Mode));           }       }       public object GetService(Type serviceType) => _engine.GetService(serviceType);       public void Dispose() => _engine.Dispose();   }
  這裡我們知道,最終提供物件並非 ServiceProvide,而是它的一個欄位  _engine 型別為 IServiceProviderEngine,在 switch 語句中,我只貼出了 Dynamic 這個分支的程式碼,因為該列舉變數 options 的預設值總是 Dynamic,這裡我們僅僅需要知道 ServiceProvider 中提供物件的核心是一個 ServiceProviderEngine,並且它的預設例項是一個 DynamicServiceProviderEngine,因為這次探險我們是去分析微軟是如何處理元資料的。這一切肯定在 DynamicServiceProviderEngine 建立過程中完成,所以我們只管尋找它的建構函式,終於,我們在父類 ServiceProviderEngine 找到了!
  internal abstract class ServiceProviderEngine : IServiceProviderEngine, IServiceScopeFactory   {       internal CallSiteFactory CallSiteFactory { get; }       protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngineCallback callback)       {           //省略了一些程式碼           CallSiteFactory = new CallSiteFactory(serviceDescriptors);           CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());           CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());       }   }
   CallSiteFactory   這裡只貼出了該類中三個欄位,然而該型別也只有該三個欄位,如果這三個欄位具體的作用理解了,那麼對於微軟如何處理元資料這一問題也就知道答案了
  internal class CallSiteFactory   {       private readonly List<ServiceDescriptor> _descriptors;       private readonly Dictionary<Type, IServiceCallSite> _callSiteCache = new Dictionary<Type, IServiceCallSite>();       private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new Dictionary<Type, ServiceDescriptorCacheItem>();       private struct ServiceDescriptorCacheItem       {           private ServiceDescriptor _item;           private List<ServiceDescriptor> _items;           //省略了一些程式碼       }   }   internal interface IServiceCallSite   {       Type ServiceType { get; }       Type ImplementationType { get; }   }
   第一個欄位 _descriptors  是一個元資料集合,我們註冊的服務都在這裡,然後我們看第三個欄位 _descriptorLookup,因為註冊服務的時候第一沒有驗證例項型別的有效性(介面,抽象類等),此外我們可以針對同一個服務進行多冊註冊,對於多次註冊的服務微軟又是如何確定建立的物件呢?這對這些問題,微軟設計了一個類概括了具體一個服務的所有註冊的例項型別 ServiceDescriptorCacheItem,具體針對一個服務,第一次註冊的元資料存在 _item 中,後續該服務的所有元資料都存在 _items,而預設的總是認同最後一個元資料。最後最難理解的就是 _callSiteCache 這個欄位了,簡單的說,它的值 IServiceCallSite 是建立服務例項的依據,包含了服務型別和例項型別。我們知道從 _descriptorLookup 獲取的是確定的例項型別,然而這個例項型別的建構函式中的型別如何建立呢,這些都在 IServiceCallSite 中體現,既然說 IServiceCallSite 是建立例項的依據,通過觀察這個介面的定義發現也並沒有和生命週期相關的屬性,有點失望!   我們回到建立 ServiceProviderEngine 建立 CallSiteFactory 的那一行程式碼,在建立CallSiteFactory 完成後,它呼叫了 Add 方法添加了兩個鍵值對。第一行程式碼的鍵是啥? IServiceProvider,是的微軟預設的允許 IServiceProvider 提供自己!   CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());   CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());   可以看到 Add 新增的鍵值對是儲存在 _callSiteCache 中的
  public void Add(Type type, IServiceCallSite serviceCallSite)   {       _callSiteCache[type] = serviceCallSite;   }
  接著我們觀察 ServiceProviderCallSite、ServiceScopeFactoryCallSite 這兩個型別,出了增加了兩個不認識的型別,並沒有其他收穫
  internal class ServiceProviderCallSite : IServiceCallSite   {       public Type ServiceType { get; } = typeof(IServiceProvider);       public Type ImplementationType { get; } = typeof(ServiceProvider);   }   internal class ServiceScopeFactoryCallSite : IServiceCallSite   {       public Type ServiceType { get; } = typeof(IServiceScopeFactory);       public Type ImplementationType { get; } = typeof(ServiceProviderEngine);   }
   關於注入的一些猜想   從上述的學習我們有了一個較為意外的收穫,IServiceProvider 是可以提供自己的,這不得不使我們猜想,IServiceProvider 具有怎樣的生命週期?如果不斷的用一個 IServiceProvider 建立一個新的,如此下去,又是如何?
  static void Main(string[] args)   {       IServiceCollection services = new ServiceCollection();       var serviceProvider = services.BuildServiceProvider();       Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IServiceProvider>(), serviceProvider.GetService<IServiceProvider>()));       var serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;       var serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;       Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IServiceProvider>(), serviceProvider2.GetService<IServiceProvider>()));       var serviceProvider3 = serviceProvider.GetService<IServiceProvider>();       var serviceProvider4 = serviceProvider.GetService<IServiceProvider>();       var serviceProvider3_1 = serviceProvider3.GetService<IServiceProvider>();       var serviceProvider4_1 = serviceProvider4.GetService<IServiceProvider>();       Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider4));       Console.WriteLine(ReferenceEquals(serviceProvider3_1, serviceProvider4_1));       Console.WriteLine(ReferenceEquals(serviceProvider3, serviceProvider3_1));       Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider));       /* True        * False        * True        * True        * True        * False        */   }
  這裡對 CreateScope 我們僅需要知道它建立的是一個具有限定範圍的容器即可,我們根據第一個輸出結果為 True 和第二個輸出結果為 False,從這點看 IServiceProvider 的生命週期和 Scoped 的定義一致,但是由於 IServiceProvider 的特殊性,它可以一直不斷的建立自己,並且他們都是同一個物件,但是和最初的 ServiceProvider 都不一樣。這讓我們又懷疑 IServiceProvider 究竟是不是 Scoped。    小結   這一節主要介紹了服務的三種生命週期,以及服務是如何註冊到元資料的,並且在建立容器的過程中,我們知道了微軟是如何進一步處理元資料的,以及建立例項物件的最終依據是 IServiceCallSite,但是想要真正的搞明白 IServiceCallSite 還必須詳細的瞭解建立容器和建立例項的過程。