.Net Core 注入学习——注册服务
解析 .Net Core 注入——注册服务
发表于:2017-10-23 10:47 作者:行走即歌 来源:51Testing软件测试网采编
字体:大 中 小 | 上一篇 | 下一篇 |我要投稿 | 推荐标签: DotNet 软件开发 dotnet
在学习 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 注入学习——注册服务的更多相关文章
- 解析 .Net Core 注入 (1) 注册服务
在学习 Asp.Net Core 的过程中,注入可以说是无处不在,对于 .Net Core 来说,它是独立的一个程序集,没有复杂的依赖项和配置文件,所以对于学习 Asp.Net Core 源码的朋友来 ...
- Asp.net core 向Consul 注册服务
Consul服务发现的使用方法:1. 在每台电脑上都以Client Mode的方式运行一个Consul代理, 这个代理只负责与Consul Cluster高效地交换最新注册信息(不参与Leader的选 ...
- 解析 .Net Core 注入——注册服务
在学习 Asp.Net Core 的过程中,注入可以说是无处不在,对于 .Net Core 来说,它是独立的一个程序集,没有复杂的依赖项和配置文件,所以对于学习 Asp.Net Core 源码的朋友来 ...
- [ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
通过<利用容器提供服务>我们知道作为依赖注入容器的IServiceProvider对象是通过调用IServiceCollection接口的扩展方法BuildServiceProvider创 ...
- 自动注册服务NET Core扩展IServiceCollection
NET Core扩展IServiceCollection自动注册服务 前言 在ASP.NET Core中使用依赖注入中使用很简单,只需在Startup类的ConfigureServices()方法中, ...
- 【ASP.NET Core】依赖注入高级玩法——如何注入多个服务实现类
依赖注入在 ASP.NET Core 中起中很重要的作用,也是一种高大上的编程思想,它的总体原则就是:俺要啥,你就给俺送啥过来.服务类型的实例转由容器自动管理,无需我们在代码中显式处理. 因此,有了依 ...
- .net core grpc consul 实现服务注册 服务发现 负载均衡(二)
在上一篇 .net core grpc 实现通信(一) 中,我们实现的grpc通信在.net core中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net ...
- SpringCloud学习--Eureka 服务注册与发现
目录 一:构建项目 二:服务注册与发现 为什么选择Eureka,请看上一篇博客 Eureka -- 浅谈Eureka 项目构建 IDEA 选择 New Project 选择 Spring Initia ...
- [ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
生命周期决定了IServiceProvider对象采用怎样的方式提供和释放服务实例.虽然不同版本的依赖注入框架针对服务实例的生命周期管理采用了不同的实现,但总的来说原理还是类似的.在我们提供的依赖注入 ...
随机推荐
- javascript预览本地图片
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- mysql 唯一键
唯一键特点: 1.唯一键在一张表中可以有多个. 2.唯一键允许字段数据为NULL,NULL可以有多个(NULL不参与比较) //一个表中允许存在多个唯一键,唯一键允许为空,在不为空的情况下,不允许重复 ...
- 使用深度学习的超分辨率介绍 An Introduction to Super Resolution using Deep Learning
使用深度学习的超分辨率介绍 关于使用深度学习进行超分辨率的各种组件,损失函数和度量的详细讨论. 介绍 超分辨率是从给定的低分辨率(LR)图像恢复高分辨率(HR)图像的过程.由于较小的空间分辨率(即尺寸 ...
- Cloudera-Manager(一) —— 基本概念及使用
概念 Cloudera Manager(简称CM)是Cloudera公司开发的一款大数据集群安装部署利器,这款利器具有集群自动化安装.中心化管理.集群监控.报警等功能,极大的提高集群管理的效率. AP ...
- Hadoop运行原理总结(详细)
本编随笔是小编个人参照个人的笔记.官方文档以及网上的资料等后对HDFS的概念以及运行原理进行系统性地归纳,说起来真的惭愧呀,自学了很长一段时间也没有对Hadoop知识点进行归纳,有时候在实战中或者与别 ...
- GWAS Catalog数据库简介
GWAS Catalog The NHGRI-EBI Catalog of published genome-wide association studies EBI负责维护的一个收集已发表的GWAS ...
- ElementUi tree 指定节点是否显示复选框
场景:树的内容是省份下面的城市有酒店 需求:只能多选酒店(为了删除它们),至于为啥不能选省份或者城市更加灵活的去删除相应酒店,这你得去问后台0.0,他只弄了根据酒店id去删除.嗯,连创建酒店的时候级联 ...
- 如何查看window 7/window 8 等系统 的激活状态?
http://www.officezhushou.com/office-key/ Office激活密钥 Win+R 输入: slmgr.vbs -dlv 显示:最为详尽的激活信息,包括:激活ID. ...
- 单例设计模式代码-bxy
struct ConnectInfo { const QObject *sender; //发送者 const char *signal_str; //发送信号 const QObject *reci ...
- linux:使用python脚本监控某个进程是否存在(不使用crontab)
背景: 需要每天定时去检测crontab进程是否启动,所以不能用crontab来启动检测脚本了,直接使用while 循环和sleep方式实现定时检测 # coding:utf-8 import os ...