淺談.Net Core DependencyInjection原始碼探究

yi念之間發表於2020-07-09

前言

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

?歡迎掃碼關注我的公眾號? 淺談.Net Core DependencyInjection原始碼探究

相關文章