这篇随笔主要记录一下ASP.NET Core团队实现默认的依赖注入容器的过程,我的理解可能并不是正确的。

DependencyInjection这个项目不大,但却是整个ASP.NET Core的基础,因为它提供了依赖注入(DI)容器的默认实现,而依赖注入贯穿整个ASP.NET Core。相关源码可以去GitHub AspNet 上下载。

要实现是一个依赖注入容器,主要是实现它添加依赖、描述依赖、存储依赖和解析依赖的能力,可以分别用Add(A), Describe(D), Store(S), Resolve(R)表示。从功能的角度来讲,分别对应着ServiceCollection,ServiceDescriptor,Service,ServiceEntry,ServiceTable,ServiceProvider,以及CallSite相关的类。

对于框架使用者来说,注册一项服务最自然的方式就是提供服务的接口和实现这个接口的服务实例,比如IEmail是用户需求的服务,而Outlook类就是服务的实例类型,用这两种信息注册一项服务是最自然的。所以ASP.NET Core团队提供了ServiceDescriptor类型来提供对服务的描述功能。

 public class ServiceDescriptor
{
/// <inheritdoc />
public ServiceLifetime Lifetime { get; } /// <inheritdoc />
public Type ServiceType { get; } /// <inheritdoc />
public Type ImplementationType { get; } /// <inheritdoc />
public object ImplementationInstance { get; } /// <inheritdoc />
public Func<IServiceProvider, object> ImplementationFactory { get; } internal Type GetImplementationType(){...} public static ServiceDescriptor Transient(){...}
public static ServiceDescriptor Singleton(){...}
public static ServiceDescriptor Scoped(){...}
}

可以看到ServiceDescriptor已经存储了服务的类型信息以及生命周期,貌似已经可以凭借着Dictionary<ServiceType, ServiceDescriptor>存储所有的服务关系了。但有个问题,如果同一个服务注册了多个服务实例类型怎么办?比如IEmail服务同时注册Outlook和GMail,该怎么存储,解析的时候又该用哪个?为了解决这个问题,ASP.NET Core团队提供了Service和ServiceEntry。不要以为Service是非常牛逼的类,其实它非常简单,Service就是一个存储ServiceDescriptor的单向链表节点,而ServiceEntry就是以Service为节点的单向链表。

     internal class ServiceEntry
{
private object _sync = new object(); public ServiceEntry(IService service)
{
First = service;
Last = service;
} public IService First { get; private set; }
public IService Last { get; private set; } public void Add(IService service)
{
lock (_sync)
{
Last.Next = service;
Last = service;
}
}
}
    internal class Service : IService
{
private readonly ServiceDescriptor _descriptor; public Service(ServiceDescriptor descriptor)
{
_descriptor = descriptor;
} public IService Next { get; set; } public ServiceLifetime Lifetime
{
get { return _descriptor.Lifetime; }
} public IServiceCallSite CreateCallSite(){...}
}

从上面的源码可以看出Service类和ServiceEntry类就是一个典型的链表节点和链表的关系,Service类中还有一个很重要的方法是CreateCallSite(),这是每个实现了IService的接口都要实现的方法。至于什么是callsite,之后会说到。

用ServiceEntry解决了一个服务的存储问题,自然一堆服务的存储就是用ServiceTable来存储。ServiceTable使用哈希表作为底层容器,以ServiceType为Key,ServiceEntry为Value存储在Dictionary中。为了优化存储结构,缓存一些已经实现过的服务,ServiceTable还添加了关于RealizedService的字段和方法。主要源码见下面:

   internal class ServiceTable
{
private readonly object _sync = new object(); private readonly Dictionary<Type, ServiceEntry> _services;
private readonly Dictionary<Type, List<IGenericService>> _genericServices;
private readonly ConcurrentDictionary<Type, Func<ServiceProvider, object>> _realizedServices = new ConcurrentDictionary<Type, Func<ServiceProvider, object>>(); //注意ServiceTable只能被ServiceDescriptor的集合初始化
public ServiceTable(IEnumerable<ServiceDescriptor> descriptors){...}
//省略了有关容器添加获取的方法
}

以上就是ASP.NET Core服务存储的相关过程,就实现来说,还是比较简单的,就是以K/V的形式,按照服务的类别存储实现了服务的相应类型(普通类,泛型类,委托等)。

仔细观察这些类型,你会发现它们都是internal级别的,那哪个才是公开类型呢?答案是ServiceCollection,这个类和Service一样,看着很重要,其实就是一个ServiceDescriptor的List,因为它实现的接口继承了IList<ServiceDescriptor>。

    public interface IServiceCollection : IList<ServiceDescriptor>
{
} public class ServiceCollection : IServiceCollection
{
//省略相关代码
}

ServiceCollection本质上是一个ServiceDescriptor的List,回忆一下,ServiceTable的构造函数正需要这样的类型啊!那个这两个类又有什么关系,解开这个谜题的关键在于这整个解决方案真正的主角:ServiceProvider。我在这之前迟迟没有提到一个依赖注入最关键的功能:解析依赖。对于一个服务A来说,它可能并不是独立的,它还在依赖服务B和服务C,而服务B又依赖服务D和服务E。。。一个合格的容器得再我们需要服务A时,能够正确的解析这个依赖链,并按照正确的顺序实例化并返回服务A。ServiceProvider是ASP.NET Core团队提供的默认的依赖注入容器。

     internal class ServiceProvider : IServiceProvider, IDisposable
{
private readonly ServiceProvider _root;
private readonly ServiceTable _table;
private bool _disposeCalled; private readonly Dictionary<IService, object> _resolvedServices = new Dictionary<IService, object>();
private List<IDisposable> _transientDisposables; private static readonly Func<Type, ServiceProvider, Func<ServiceProvider, object>> _createServiceAccessor = CreateServiceAccessor; public ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors)
{
_root = this;
_table = new ServiceTable(serviceDescriptors); _table.Add(typeof(IServiceProvider), new ServiceProviderService());
_table.Add(typeof(IServiceScopeFactory), new ServiceScopeService());
_table.Add(typeof(IEnumerable<>), new OpenIEnumerableService(_table));
}
public object GetService(Type serviceType){...}
internal static Func<ServiceProvider, object> RealizeService(ServiceTable table, Type serviceType, IServiceCallSite callSite){...}
internal IServiceCallSite GetServiceCallSite(Type serviceType, ISet<Type> callSiteChain){...}
//省略了一些有关服务生存周期管理的方法以及一些其他私有方法
}

首先需要注意的是,它有一个ServiceTable类型的字段,所以一个ServiceProvider不仅是一个解析器,而且是一个容器,是一个依赖注入容器。第二点,仔细观察它的构造函数,你会发现它向table字段中添加了三个服务,而且这三个服务是自添加的,每个ServiceProvider都有。再研究一下这些服务的名字,更加有意思,ServiceProviderService!!也就是说ServiceProvider也是一种服务,解析服务也是一种服务,容器也是一种服务。这意味着我们可以使用其他依赖注入容器。第三点,也是最重要的一点,这个Service,RealizedService,ResolvedService以及我们一直避而不谈的callsite究竟是啥?

当我们以类型的方式描述一种服务时,它就是所谓的Service,这时它的信息全部以元数据的方式存储。

每一个Service都有一个CreateCallSite方法,所谓callsite,直接翻译是“调用点”,但更好的理解方式我觉得是元数据和服务实例之间的桥梁,而如果一种Service元数据变成了Func<ServiceProvider, object>委托,我们就把它称为RealizedService,在Provider的table里面,有这么一个字段专门管理RealizedService。那Func<ServiceProvider, object>委托又怎么理解呢?这种委托可以看作是服务的兑换券,它还不是解析的服务,但是离它很近了!因为只要把ServiceProvider传进去,我们就能得到解析过的Service。

如果把Func<ServiceProvider, object>委托当成兑换券,那么ServiceProvider就是兑换人,把兑换券拿给兑换人,我们就能得到object类型的服务,这种服务称之为ResolvedService,在ServiceProvider中专门有一个字段缓存这些解析过的服务。callsite的Invoke(provider)方法得到一个服务实例(Resolved),而callsite的Build().Complie()方式可以得到Func<ServiceProvider, object>委托(Realized)。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

总结一下整个流程:

  1. 当我们注册一个服务时,最自然是通过它的类型和它的实现类型来注册,比如IEmail类型和Outlook类型,所以要用到ServiceDescriptor;
  2. ServiceDescriptor包装一下,摇身一变成为Service,并且得到了一个关键方法CreateCallSite();
  3. 为什么要callsite这种东西,主要是为了配合Provider管理服务的生命周期,以及实现一些特殊的解析服务的功能。如上所述,callsite的Invoke()得到ResolvedService,callsite的Build()方法得到RealizedService;
  4. 由Provider根据生命周期负责回收服务。

ASP.NET Core 源码阅读笔记(1) ---Microsoft.Extensions.DependencyInjection的更多相关文章

  1. ASP.NET Core 源码阅读笔记(2) ---Microsoft.Extensions.DependencyInjection生命周期管理

    在上一篇文章中我们主要分析了ASP.NET Core默认依赖注入容器的存储和解析,这一篇文章主要补充一下上一篇文章忽略的一些细节:有关服务回收的问题,即服务的生命周期问题.有关源码可以去GitHub上 ...

  2. ASP.NET Core 源码阅读笔记(5) ---Microsoft.AspNetCore.Routing路由

    这篇随笔讲讲路由功能,主要内容在项目Microsoft.AspNetCore.Routing中,可以在GitHub上找到,Routing项目地址. 路由功能是大家都很熟悉的功能,使用起来也十分简单,从 ...

  3. ASP.NET Core 源码阅读笔记(3) ---Microsoft.AspNetCore.Hosting

    有关Hosting的基础知识 Hosting是一个非常重要,但又很难翻译成中文的概念.翻译成:寄宿,大概能勉强地传达它的意思.我们知道,有一些病毒离开了活体之后就会死亡,我们把那些活体称为病毒的宿主. ...

  4. CI框架源码阅读笔记5 基准测试 BenchMark.php

    上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功 ...

  5. CI框架源码阅读笔记4 引导文件CodeIgniter.php

    到了这里,终于进入CI框架的核心了.既然是“引导”文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.c ...

  6. CI框架源码阅读笔记3 全局函数Common.php

    从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap ...

  7. CI框架源码阅读笔记2 一切的入口 index.php

    上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里再次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中, ...

  8. Three.js源码阅读笔记-5

    Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...

  9. Mina源码阅读笔记(四)—Mina的连接IoConnector2

    接着Mina源码阅读笔记(四)-Mina的连接IoConnector1,,我们继续: AbstractIoAcceptor: 001 package org.apache.mina.core.rewr ...

随机推荐

  1. AspNetPager分页控件的使用

    下面所记得东西仅仅是使用方法,详细知识点请看链接:http://www.webdiyer.com/Controls/AspNetPager/Downloads 首先:从网站上下载并安装控件 下载地址: ...

  2. postgresql 常用数据库命令

    连接数据库, 默认的用户和数据库是postgrespsql -U user -d dbname 切换数据库,相当于MySQL的use dbname\c dbname列举数据库,相当于mysql的sho ...

  3. 让tomcat启动更快的设置

    http://wiki.apache.org/tomcat/HowTo/FasterStartUp#Entropy_Source 关于随机数的"熵源"(entropy source ...

  4. [Idea] idea打不开项目,原因很莫名

    由于项目是gitlab上存储的,所以下下来之后,之前遇到过,以为是重新下载之后master上面没有内容导致无法正常打开,这种情况,切换一下master再打开即可: 但是这次遇到的问题不是这种情况, 使 ...

  5. jquery中ajax返回值无法传递到上层函数

    function通过ajax调用获取后台数据,结果返回出来的结果均为空,代码如下: function chart_coinbase_getdata() { var test = {postdata:& ...

  6. VS低版本打开高版本解决方案(如08打开10、12、13版本vs编译的项目)

    一.vs2005打开vs2008编译的项目:1.用记事本打开sln文件,将: Microsoft Visual Studio Solution File, Format Version 10.00 # ...

  7. 毕业论文—使用js将canvas保存为图片文件,并且自定义文件名

    该文章引用http://blog.csdn.net/qq547276542/article/details/51906741 1.从canvas中直接提取图片元数据 // 图片导出为 png 格式 v ...

  8. 关于 Lo、Hi、LoWord、HiWord

    Cardinal 是 4 字节无符号的整型, 先看一个例数: Cardinal 例数: 4277991664 按字节划分: 第四字节 第三字节 第二字节 第一字节 二进制: 11111110 1111 ...

  9. C#与C/C++的交互zz

    C#与C++交互,总体来说可以有两种方法: 利用C++/CLI作为代理中间层 利用PInvoke实现直接调用 第一种方法:实现起来比较简单直观,并且可以实现C#调用C++所写的类,但是问题是MONO构 ...

  10. 简述JavaScript对象、数组对象与类数组对象

    问题引出 在上图给出的文档中,用JavaScript获取那个a标签,要用什么办法呢?相信第一反应一定是使用document.getElementsByTagName('a')[0]来获取.同样的,在使 ...