解析 .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 还必须详细的了解建立容器和建立实例的过程。

相关文章
相关标签/搜索