淺談.Net Core DependencyInjection源碼探究

前言

    相信使用過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倉庫中具體地址是//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的情況。
  • 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倉庫的位置//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的方式。有很多核心的源碼並沒有講解到,因為怕自己理解不夠,就不誤導大家了。我在上文中涉及到源碼的地方基本上都加了源碼的連接,可以直接點進去查看源碼,之前源碼探究相關的文章也都是一樣,可能之前有許多同學沒有注意到。主要原因是我粘貼出來的代碼有刪減,最重要的還是怕自己理解不到位,誤導了大家,這樣就能用過點擊自己查看源碼了。如有你有更好的理解,或者覺得我講解的理解不到的地方,歡迎評論區溝通交流。

👇歡迎掃碼關注我的公眾號👇