在学习 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 中,注入描述为为三个过程,注册服务->创建容器->创建对象,所以我也会分为三个模块来介绍
  注入元数据
  如果接触过 .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 并不会校验实例类型的可创建性(验证其是否是抽象类,接口)
  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));
  }
  此外,微软还提供了“服务实例”(提供一个服务类型,一个实例对象)以及“服务实例工厂”(提供一个服务类型,一个实例对象工厂)的注册方式,前者只供单例服务使用,使用起来也很简单
  services.AddTransient<ITransient>(_=>new Transient());
  services.AddSingleton<ISingleton>(new Singleton());
  关于 ServiceDescriptor,还有一个要说的就是服务的生命周期了,使用 AddSingleton、AddScoped、AddTransient 三种方式注册的服务在 ServiceDescriptor 中的 LifeTime 属性分别对应下面这个枚举类型
  public enum ServiceLifetime
  {
      Singleton,
      Scoped,
      Transient
  }
  1、Transient:每次从容器 (IServiceProvider)中获取的时候都是一个新的实例
  2、Singleton:每次从同根容器中(同根 IServiceProvider)获取的时候都是同一个实例
  3、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 还必须详细的了解创建容器和创建实例的过程。

解析 .Net Core 注入——注册服务的更多相关文章

  1. 解析 .Net Core 注入 (1) 注册服务

    在学习 Asp.Net Core 的过程中,注入可以说是无处不在,对于 .Net Core 来说,它是独立的一个程序集,没有复杂的依赖项和配置文件,所以对于学习 Asp.Net Core 源码的朋友来 ...

  2. 解析 .Net Core 注入 (3) 创建对象

    回顾 通过前两节的学习,我们知道 IServiceCollection 以元数据(ServiceDescriptor)的形式存放着用户注册的服务,它的 IServiceCollection 的拓展方法 ...

  3. 解析 .Net Core 注入 (2) 创建容器

    在上一节的学习中,我们已经知道了通过 IServiceCollection 拓展方法创建 IServiceProvider 默认的是一个类型为 ServiceProvider 对象,并且实际提供创建对 ...

  4. .Net Core 注入学习——注册服务

    解析 .Net Core 注入——注册服务发表于:2017-10-23 10:47 作者:行走即歌 来源:51Testing软件测试网采编字体:大 中 小 | 上一篇 | 下一篇 |我要投稿 | 推荐 ...

  5. 依赖注入[7]: .NET Core DI框架[服务注册]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.服务注册就是创建出现相应的ServiceDescriptor对象并将其添加到 ...

  6. .NET CORE学习笔记系列(2)——依赖注入[7]: .NET Core DI框架[服务注册]

    原文https://www.cnblogs.com/artech/p/net-core-di-07.html 包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IS ...

  7. 自动注册服务NET Core扩展IServiceCollection

    NET Core扩展IServiceCollection自动注册服务 前言 在ASP.NET Core中使用依赖注入中使用很简单,只需在Startup类的ConfigureServices()方法中, ...

  8. 依赖注入[8]: .NET Core DI框架[服务消费]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.当需要消费某个服务实例的时候,我们只需要指定服务类型调用IServicePr ...

  9. Asp.net core 向Consul 注册服务

    Consul服务发现的使用方法:1. 在每台电脑上都以Client Mode的方式运行一个Consul代理, 这个代理只负责与Consul Cluster高效地交换最新注册信息(不参与Leader的选 ...

随机推荐

  1. Linux常用命令行补充——持续更新

    1.查看文件夹大小 =>ls -lht 路径 ls -lht /opt/jars 2.查看文件大小 =>du -sh / du -sh /opt/jars/calllog.csv 3.编辑 ...

  2. Flume配置文件写法总结

    一.agent 第一步是定义agent(代理)及agent下的sources.channels.sinks的简称,如下: a1.sources = r1 a1.sinks = k1 a1.channe ...

  3. CentOS系统找不到setup命令工具的解决方法

    如果你的CentOS系统中没有setup命令,很有可能是因为你安装CentOS系统时采用了最小化安装(minimal).这时,你执行setup命令时,就会报错: 错误信息: 1[root@localh ...

  4. Visual Studio 2019及其注册码

    Visual Studio 2019 更快地进行代码编写.更智能地执行操作.使用同类最佳IDE 创建未来.     下载Visual Studio         使用从初始设计到最终部署的完整工具集 ...

  5. HDU 5036 Explosion (传递闭包+bitset优化)

    <题目链接> 题目大意: 一个人要打开或者用炸弹砸开所有的门,每个门后面有一些钥匙,一个钥匙对应一个门,告诉每个门里面有哪些门的钥匙.如果要打开所有的门,问需要用的炸弹数量为多少. 解题分 ...

  6. Pytorch安装(基于anaconda虚拟环境)

    Pytorch安装倒腾了一上午终于搞定,记录一下安装过程. 1. 首先尝试官网的安装方式,但是网速太慢了. 除去cudnn100, torchvision和pytorch三个文件,其余可以直接从清华镜 ...

  7. 一个简单需求:HashMap实现相同key存入数据后不被覆盖

    做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 看似是一个简单的问题,其实里面包含很多的东西! 需求: 实现一个在HashMap中存入(任意类型)相同的key值后,key ...

  8. shell 运算符

    shell的逻辑运算符 涉及有以下几种类型,因此只要适当选择,可以解决我们很多复杂的判断,达到事半功倍效果. 一.逻辑运算符 逻辑卷标 表示意思 1. 关于档案与目录的侦测逻辑卷标! -f 常用!侦测 ...

  9. photoshop实现倾斜图片的修正

    第一天学习Photoshop,了解到了Photoshop对图片的处理,下面是实现一个倾斜图片修正的两种方法: 举例图片: 第一种方法:1.利用吸管中的标尺工具量倾斜度数: 2.利用旋转图像,旋转对应的 ...

  10. asp.net core自定义模型验证——前端验证

    转载请注明出处:http://www.cnblogs.com/zhiyong-ITNote/ 官方网站:https://docs.microsoft.com/zh-cn/aspnet/core/mvc ...