解析 .Net Core 注入 (1) 註冊服務

在學習 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 中,注入描述爲爲三個過程,註冊服務->建立容器->建立對象,因此我也會分爲三個模塊來介紹ide

注入元數據

若是接觸過 .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 並不會校驗實例類型的可建立性(驗證其是不是抽象類,接口)ui

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));
}

此外,微軟還提供了「服務實例」(提供一個服務類型,一個實例對象)以及「服務實例工廠」(提供一個服務類型,一個實例對象工廠)的註冊方式,前者只供單例服務使用,使用起來也很簡單this

services.AddTransient<ITransient>(_=>new Transient());
services.AddSingleton<ISingleton>(new Singleton());

關於 ServiceDescriptor,還有一個要說的就是服務的生命週期了,使用 AddSingleton、AddScoped、AddTransient 三種方式註冊的服務在 ServiceDescriptor 中的 LifeTime 屬性分別對應下面這個枚舉類型spa

public enum ServiceLifetime
{
    Singleton,
    Scoped,
    Transient
}

一、Transient:每次從容器 (IServiceProvider)中獲取的時候都是一個新的實例

二、Singleton:每次從同根容器中(同根 IServiceProvider)獲取的時候都是同一個實例

三、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 還必須詳細的瞭解建立容器和建立實例的過程。

相關文章
相關標籤/搜索