前言
相信使用過Asp.Net Core開發架構的人對自帶的DI架構已經相當熟悉了,很多剛開始接觸.Net Core的時候覺得不适應,主要就是因為Core預設內建它的原因。它是Asp.Net Core基礎核心架構之一,對于Asp.Net Core來說DI就靈魂,已經深入到這架構的骨髓裡了。對于IOC和DI,可能每個人都能說出自己的了解。IOC全稱是Inversion of Control翻譯成中文叫控制反轉,簡單的說就是把對象的控制權反轉到IOC容器中,由IOC管理其生命周期。DI全稱是DependencyInjection翻譯成中文叫依賴注入,就是IOC容器把你依賴的子產品通過注入的方式提供給你,而不是你自己主動去擷取,其形式主要分為構造注入和屬性注入,Core自帶的DI隻支援構造注入,至于為什麼,最多的說法就是構造注入能使得依賴變得更清晰,我既然依賴你,那麼我執行個體化的時候你就必須得出現。而構造函數恰恰就承擔着這種責任。
簡單介紹
很多人接觸它的時候應該都是從Asp.Net Core學習過程中開始的。其實它本身對Asp.Net Core并無依賴關系,Asp.Net Core依賴DI,但是這套架構本身并不隻是可以提供給Asp.Net Core使用,它是一套獨立的架構,開源在微軟官方Github的extensions倉庫中具體位址是https://github.com/dotnet/extensions/tree/v3.1.5/src/DependencyInjection。關于如何使用,這裡就不再多說了,相信大家都非常清楚了。那咱們就說點不一樣的。
服務注冊
我們都知道提供注冊的服務名稱叫IServiceCollection,我們大部分情況下主要使用它的AddScoped、AddTransient、AddSingleton來完成注冊。我們就先檢視一下IServiceCollection接口的具體實作,找到源碼位置
public interface IServiceCollection : IList<ServiceDescriptor> { }
(⊙o⊙)…額,你并沒有看錯,這次我真沒少貼代碼,其實IServiceCollection本質就是IList,而且并沒有發現AddScoped、AddTransient、AddSingleton蹤影,說明這幾個方法是擴充方法,我們找到ServiceCollectionServiceExtensions擴充類的位置,我們平時用的方法都在這裡,由于代碼非常多這裡就不全部粘貼出來了,我們隻粘貼AddTransient相關的,AddScoped、AddSingleton的實作同理
/// <summary> /// 通過泛型注冊 /// </summary> 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)); } /// <summary> /// 根據類型注冊 /// </summary> public static IServiceCollection AddTransient( this IServiceCollection services, Type serviceType, Type implementationType) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (serviceType == null) { throw new ArgumentNullException(nameof(serviceType)); } if (implementationType == null) { throw new ArgumentNullException(nameof(implementationType)); } return Add(services, serviceType, implementationType, ServiceLifetime.Transient); } /// <summary> /// 根據類型執行個體來自工廠注冊方法 /// </summary> public static IServiceCollection AddTransient( this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (serviceType == null) { throw new ArgumentNullException(nameof(serviceType)); } if (implementationFactory == null) { throw new ArgumentNullException(nameof(implementationFactory)); } return Add(services, serviceType, implementationFactory, ServiceLifetime.Transient); }
通過以上代碼我們可以得到兩個結論,一是注冊服務的方法本質都是在調用Add重載的兩個方法,二是聲明周期最終還是通過ServiceLifetime來控制的AddScoped、AddTransient、AddSingleton隻是分文别類的進行封裝而已,我們來看ServiceLifetime的源碼實作
public enum ServiceLifetime { /// <summary> /// 指定将建立服務的單個執行個體。 /// </summary> Singleton, /// <summary> /// 指定每個作用域建立服務的新執行個體。 /// </summary> Scoped, /// <summary> /// 指定每次請求服務時都将建立該服務的新執行個體。 /// </summary> Transient }
這個枚舉是為了枚舉我們注冊服務執行個體的聲明周期的,非常清晰不在過多講述,接下來我們看核心的兩個Add方法的實作
private static IServiceCollection Add( IServiceCollection collection, Type serviceType, Type implementationType, ServiceLifetime lifetime) { var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); collection.Add(descriptor); return collection; } private static IServiceCollection Add( IServiceCollection collection, Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime) { var descriptor = new ServiceDescriptor(serviceType, implementationFactory, lifetime); collection.Add(descriptor); return collection; }
通過這兩個核心方法我們可以非常清晰的了解到注冊的本質其實就是建構ServiceDescriptor執行個體然後添加到IServiceCollection即IList中,這裡我們都是列舉的根據執行個體去注冊抽象的類型,還有一種是隻注冊具體類型或者具體執行個體的方法,這個是怎麼實作的呢。
public static IServiceCollection AddTransient( this IServiceCollection services, Type serviceType) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (serviceType == null) { throw new ArgumentNullException(nameof(serviceType)); } //把自己注冊給自己 return services.AddTransient(serviceType, serviceType); }
通過這個方法我們就可以看到其實注冊單類型的方法,也是通過調用的注入執行個體到抽象的方法,隻不過是将自己注冊給了自己。
好了,抽象和擴充方法我們就先說到這裡,接下來我們來看IServiceCollection的實作類ServiceCollection的實作
public class ServiceCollection : IServiceCollection { private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>(); public int Count => _descriptors.Count; public bool IsReadOnly => false; public ServiceDescriptor this[int index] { get { return _descriptors[index]; } set { _descriptors[index] = value; } } public void Clear() { _descriptors.Clear(); } public bool Contains(ServiceDescriptor item) { return _descriptors.Contains(item); } public void CopyTo(ServiceDescriptor[] array, int arrayIndex) { _descriptors.CopyTo(array, arrayIndex); } public bool Remove(ServiceDescriptor item) { return _descriptors.Remove(item); } public IEnumerator<ServiceDescriptor> GetEnumerator() { return _descriptors.GetEnumerator(); } void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item) { _descriptors.Add(item); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int IndexOf(ServiceDescriptor item) { return _descriptors.IndexOf(item); } public void Insert(int index, ServiceDescriptor item) { _descriptors.Insert(index, item); } public void RemoveAt(int index) { _descriptors.RemoveAt(index); } }
這個類就非常清晰,也非常簡單了。ServiceCollection承載了一個List的集合,由于實作了IList接口,是以該類實作了接口的方法,實作了對List集合的操作,其核心就是ServiceDescriptor服務描述類,我們看一下大緻的源碼。
public class ServiceDescriptor { public ServiceDescriptor( Type serviceType, Type implementationType, ServiceLifetime lifetime) : this(serviceType, lifetime) { ImplementationType = implementationType; } public ServiceDescriptor( Type serviceType, object instance) : this(serviceType, ServiceLifetime.Singleton) { ImplementationInstance = instance; } public ServiceDescriptor( Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime) : this(serviceType, lifetime) { ImplementationFactory = factory; } private ServiceDescriptor(Type serviceType, ServiceLifetime lifetime) { Lifetime = lifetime; ServiceType = serviceType; } public ServiceLifetime Lifetime { get; } public Type ServiceType { get; } public Type ImplementationType { get; } public object ImplementationInstance { get; } public Func<IServiceProvider, object> ImplementationFactory { get; } }
這裡我們隻是粘貼了初始化的方法,通過這個初始化我們得到了,本質其實就是給描述具體注冊的Lifetime、ServiceType、ImplementationType、ImplementationInstance、ImplementationFactory指派。在平時的使用中,我們在注冊服務的時候還會用到這種注冊方式
services.Add(ServiceDescriptor.Scoped<IPersonService, PersonService>()); //services.Add(ServiceDescriptor.Scoped(typeof(IPersonService),typeof(PersonService))); //或 services.Add(ServiceDescriptor.Transient<IPersonService, PersonService>()); //services.Add(ServiceDescriptor.Transient(typeof(IPersonService), typeof(PersonService))); //或 services.Add(ServiceDescriptor.Singleton<IPersonService, PersonService>()); //services.Add(ServiceDescriptor.Singleton(typeof(IPersonService), typeof(PersonService)));
這種注冊方式是通過ServiceDescriptor自身的操作去注冊相關執行個體,我們拿出來其中一個Transient看一下具體實作
public static ServiceDescriptor Transient<TService, TImplementation>() where TService : class where TImplementation : class, TService { //都是在調用Describe return Describe<TService, TImplementation>(ServiceLifetime.Transient); } public static ServiceDescriptor Transient(Type service, Type implementationType) { //都是在調用Describe return Describe(service, implementationType, ServiceLifetime.Transient); } public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime) { //還是傳回ServiceDescriptor執行個體 return new ServiceDescriptor(serviceType, implementationType, lifetime); } public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime) { //還是傳回ServiceDescriptor執行個體 return new ServiceDescriptor(serviceType, implementationFactory, lifetime); }
通過這個我們就可以了解到ServiceDescriptor.Scoped、ServiceDescriptor.Singleton、ServiceDescriptor.Singleton其實是調用的Describe方法,Describe的本身還是去執行個體化ServiceDescriptor,殊途同歸,隻是多了種寫法,最終還是去建構ServiceDescriptor。通過這麼多源碼的分析得出的結論就一點IServiceCollection注冊的本質就是在建構ServiceDescriptor集合。
服務提供
上面我們了解到了服務注冊相關,至于服務是怎麼提供出來的,大家應該都是非常熟悉了其實是根據IServiceCollection建構出來的
IServiceProvider serviceProvider = services.BuildServiceProvider();
BuildServiceProvider并不是IServiceCollection的自帶方法,是以也是來自擴充方法,找到ServiceCollectionContainerBuilderExtensions擴充類,最終都是在執行這個方法
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options) { return new ServiceProvider(services, options); }
BuildServiceProvider的時候需要傳遞ServiceProviderOptions這個類主要是配置是否校驗作用域和提供的執行個體來自于那種提供引擎使用
public class ServiceProviderOptions { internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions(); /// <summary> /// 是夠在編譯的時候校驗作用域範圍檢查 /// </summary> public bool ValidateScopes { get; set; } /// <summary> /// 是夠在編譯的時候校驗作用域範圍檢查 /// </summary> public bool ValidateOnBuild { get; set; } /// <summary> /// 配置使用那種方式提供ServiceProvider的承載的具體執行個體 /// </summary> internal ServiceProviderMode Mode { get; set; } = ServiceProviderMode.Default; } internal enum ServiceProviderMode { Default, Dynamic, Runtime, Expressions, ILEmit }
作用域範圍檢查還是非常嚴格的,不開啟的也會有一定的依賴規則,簡單總結一下
- 如果開啟了範圍檢查,有依賴關系的模型如果生命周期不一緻就會報錯,如果不存Scope聲明但是擷取AddScoped也是會有異常的
- 如果不開啟範圍檢查,如果生命周期長的依賴生命周期短的,那麼被依賴的模型将會被提升和依賴模型同等的生命周期。如果生命周期短的模型依賴生命周期長的模型,将保持和注冊時候的生命周期一緻。
接下來我們檢視一下服務提供核心IServiceProvider的實作,這個接口隻包含一個抽象,那就是根據"注冊類型"擷取具體執行個體,其他擷取執行個體的方法都是根據這個方法擴充而來
public interface IServiceProvider { object GetService (Type serviceType); }
ServiceProvider是IServiceProvider的預設實作類,它是擷取注冊執行個體的預設出口類,我們隻看提供服務相關的
public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback, IAsyncDisposable { private readonly IServiceProviderEngine _engine; private readonly CallSiteValidator _callSiteValidator; internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options) { IServiceProviderEngineCallback callback = null; if (options.ValidateScopes) { callback = this; _callSiteValidator = new CallSiteValidator(); } //根據ServiceProviderMode的值判斷才有那種方式去執行個體化對象 switch (options.Mode) { //預設方式 case ServiceProviderMode.Default: if (RuntimeFeature.IsSupported("IsDynamicCodeCompiled")) { _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback); } else { _engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback); } break; case ServiceProviderMode.Dynamic: _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback); break; case ServiceProviderMode.Runtime: _engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback); break; //if IL_EMIT case ServiceProviderMode.ILEmit: _engine = new ILEmitServiceProviderEngine(serviceDescriptors, callback); break; case ServiceProviderMode.Expressions: _engine = new ExpressionsServiceProviderEngine(serviceDescriptors, callback); break; default: throw new NotSupportedException(nameof(options.Mode)); } //判斷是否開啟編譯時範圍校驗 if (options.ValidateOnBuild) { List<Exception> exceptions = null; foreach (var serviceDescriptor in serviceDescriptors) { try { _engine.ValidateService(serviceDescriptor); } catch (Exception e) { } } } } /// <summary> /// 通過IServiceProviderEngine擷取具體執行個體的方法 /// </summary> public object GetService(Type serviceType) => _engine.GetService(serviceType); }
在這個類裡,關于提供具體執行個體的操作還是非常清晰的,關于更深的IServiceProviderEngine這裡就不過多介紹了,有興趣的可以自行在GitHub上查閱。
關于Scope問題
在聲明周期裡Scope是比較特殊也是比較抽象的一個,我們使用的時候是通過目前serviceProvider建立子作用域
using (IServiceScope scope = serviceProvider.CreateScope()) { IServiceProvider scopeProvider = scope.ServiceProvider; }
它大概的思路就是在目前容器中建立一個作用域,scope.ServiceProvider來擷取這個子容器作用域裡的執行個體。Singleton類型的執行個體直接去根容器擷取,是以和目前子容器作用域無關。Scoped類型的執行個體,在目前作用域内唯一,無論擷取多少次傳回的都是同一個執行個體。Transient類型的隻要去擷取都是傳回新的執行個體。目前IServiceScope釋放的時候Scoped類型的執行個體也會被釋放,注意!!!Transient類型的執行個體也是在目前IServiceScope Dispose的時候去釋放,盡管你每次擷取的時候都是新的執行個體,但是釋放的時候都是統一釋放的。在目前ServiceScope内你可以繼續建立目前Scope的IServiceScope。其實通過這裡也不難發現根容器的Scoped其實就是等同于Singleton,其生命周期都是和應用程式保持一緻。
Scope問題在如果寫控制台之類的程式其作用可能不是很明顯,除非有特殊的要求,在Asp.Net Core中使用還是比較深入的。Asp.Net Core在啟動的時候會建立serviceProvider,這個serviceProvider的Scope是跟随程式的生命周期一緻的,它是作為所有服務執行個體的根容器。在Asp.Net Core中有幾種情況的執行個體和請求無關也就是說在程式運作期間是單例情況的,我們使用的時候需要注意的地方
- 通過Startup.cs的構造函數注入的IHostEnvironment、IWebHostEnvironment、IConfiguration
- 在Startup.cs類中的Configure方法注入的
- 使用約定方式自定義的中間件,是在程式初始化的時候被執行的是以根據約定方式定義的中間件的構造函數注入的也是單例的。
其實就一點,在程式初始化過程中建立的類大部分都是和請求無關的,通常這一類方法或者具體的執行個體注入的依賴都是和程式生命周期保持一緻的,即單例模式。Asp.Net Core在每次處理請求的時候會在根容器建立一個Scope範圍的ServiceProvider,也就是我們所說的Asp.Net Core在每次請求過程中是唯一的情況。
- 自定義實作了IMiddleware的中間件,且生命周期為Scoped的情況。
- 中間件中Invoke或InvokeAsync注入的相關執行個體,且注冊的時候為Scoped的情況。
- 通過HttpContext.RequestServices擷取的服務,且注冊的時候為Scoped的情況。
-
Controller中或者為Controller提供服務的相關類,比如EF SQLConnection或其他連接配接服務相關,或者自定義的Service等,且注冊的時候為Scoped的情況。
這裡說明一點,預設情況下Controller并不是通過容器建立的,而是通過反射建立的。如果需要将Controller也托管到容器中,需要使用services.AddControllers().AddControllersAsServices()的方式,這個操作在使用Autofac容器的時候在Controller中使用屬性注入是必不可少的。
- 還有就是通過Inject注冊到RazorPage視圖頁面中的情況。
關于UseServiceProviderFactory
UseServiceProviderFactory方法主要是為我們提供了替換預設容器的操作,通過這個方法可以将三方的IOC架構結合進來比如Autofac。我們可以檢視UseServiceProviderFactory具體的實作,了解它的工作方式。這個方法來自HostBuilder類
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) { _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory))); return this; }
我們找到_serviceProviderFactory定義的地方,預設值就是為ServiceFactoryAdapter傳遞了DefaultServiceProviderFactory執行個體。
private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
繼續查找ServiceFactoryAdapter的大緻核心實作
internal class ServiceFactoryAdapter<TContainerBuilder> : IServiceFactoryAdapter { private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory; public object CreateBuilder(IServiceCollection services) { return _serviceProviderFactory.CreateBuilder(services); } public IServiceProvider CreateServiceProvider(object containerBuilder) { return _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder); } }
通過查找HostBuilder中這段源碼我們可以知道ServiceFactoryAdapter建立出來的容器是供整個Host使用的。也就是說我們在程式中使用的容器相關的都是由它提供的。

接下來我們看下預設的DefaultServiceProviderFactory的大緻實作。找到源碼位置
public class DefaultServiceProviderFactory : IServiceProviderFactory<IServiceCollection> { public IServiceCollection CreateBuilder(IServiceCollection services) { return services; } public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) { return containerBuilder.BuildServiceProvider(_options); } }
沒啥邏輯,其實就是把預設的IServiceCollection和IServiceProvider通過工廠的形式提供出來。這麼做的目的隻有一個,就是降低依賴的耦合度友善我們能夠介入第三方的IOC架構。口說無憑,接下來我們就看一下Autofac是怎麼适配進來的。我們在GitHub上找到Autofac.Extensions.DependencyInjection倉庫的位置https://github.com/autofac/Autofac.Extensions.DependencyInjection,找到Autofac中IServiceProviderFactory實作類AutofacServiceProviderFactory,看看他是如何适配到預設的IOC架構的
public class AutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder> { private readonly Action<ContainerBuilder> _configurationAction; public AutofacServiceProviderFactory(Action<ContainerBuilder> configurationAction = null) { _configurationAction = configurationAction ?? (builder => { }); } public ContainerBuilder CreateBuilder(IServiceCollection services) { //由于是使用Autofac本身的容器去工作,是以傳回的Autofac承載類ContainerBuilder var builder = new ContainerBuilder(); //将現有的IServiceCollection中注冊的執行個體托管到ContainerBuilder中 builder.Populate(services); //這一步是我們自定義注入到Autofac方法的委托,及我們在Startup類中定義的 //public void ConfigureContainer(ContainerBuilder builder)方法 _configurationAction(builder); return builder; } public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder) { if (containerBuilder == null) throw new ArgumentNullException(nameof(containerBuilder)); //擷取Container容器,因為接下來要使用擷取執行個體的方法了 var container = containerBuilder.Build(); //這個類實作了IServiceProvider接口 //實作了public object GetService(Type serviceType)方法從Autofac的Container中擷取執行個體 return new AutofacServiceProvider(container); } }
IServiceProviderFactory的工作其實就是适配符合我們使用的擴充卡模式,其核心就是用你的容器去托管注冊到IServiceCollection中的服務。然後用你的容器去建構IServiceProvider執行個體。
總結
通過以上我們對自帶的DependencyInjection工作方式有了一定的了解,而且其擴充性非常強,能夠使我們通過自己的方式去建構服務注冊和注入,我們以Autofac為例講解了三方容器內建到自帶IOC的方式。有很多核心的源碼并沒有講解到,因為怕自己了解不夠,就不誤導大家了。我在上文中涉及到源碼的地方基本上都加了源碼的連接配接,可以直接點進去檢視源碼,之前源碼探究相關的文章也都是一樣,可能之前有許多同學沒有注意到。主要原因是我粘貼出來的代碼有删減,最重要的還是怕自己了解不到位,誤導了大家,這樣就能用過點選自己檢視源碼了。如有你有更好的了解,或者覺得我講解的了解不到的地方,歡迎評論區溝通交流。
👇歡迎掃碼關注我的公衆号👇