历数依赖注入的N种玩法
历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面。在ASP.NET Core应用中基于依赖注入的编程主要涉及到两个方面,它们分别是将服务注册到ServiceCollection中,和采用注入的方式利用ServiceProvider提供我们所需的服务。我们先来讨论ASP.NET Core应用中如何进行服务注册。[本文已经同步到《ASP.NET Core框架揭秘》之中]
目录
一、服务注册
系统自动注册的服务
手工注册的服务
二、以注入的形式提取服务
启动类型的构造函数和Configure方法种注入服务
中间件类型的构造函数和Invoke方法中注入服务
Controller类型的构造函数中注入服务
View中注入服务
三、与第三方DI框架的整合
一、服务注册
就注册的主体来划分,ASP.NET Core应用中注册的服务大体可以分为两种类型,一种是WebHostBuilder在创建WebHost之前自动注册的服务,这些服务确保了后续管道能够顺利构建并能提供基本的请求处理能力。另一种则是用户根据自身的需要注册的,如果系统自动注册的服务不符合我们的需求,我们也可以注册自己的服务来覆盖它。
系统自动注册的服务
那么系统在构建ASP.NET Core管道的时候到底自行注册了那些服务呢?对于这个问题,我们不用去查看相关的源代码如何编写,而只需要编写如下一个简单的程序就可以将这些服务输出来。
1: public class Program
2: {
3: public static void Main()
4: {
5: Console.WriteLine("{0,-30}{1,-15}{2}", "Service Type", "Lifetime", "Implementation");
6: Console.WriteLine("-------------------------------------------------------------");
7:
8: new WebHostBuilder()
9: .UseKestrel()
10: .Configure(app => { })
11: .ConfigureServices(svcs =>
12: {
13: IServiceProvider serviceProvider = svcs.BuildServiceProvider();
14: foreach (var svc in svcs)
15: {
16: if (null != svc.ImplementationType)
17: {
18: Console.WriteLine("{0,-30}{1,-15}{2}", svc.ServiceType.Name, svc.Lifetime, svc.ImplementationType.Name);
19: continue;
20: }
21: object instance = serviceProvider.GetService(svc.ServiceType);
22: Console.WriteLine("{0,-30}{1,-15}{2}", svc.ServiceType.Name, svc.Lifetime, instance.GetType().Name);
23: }
24:
25: })
26: .Build();
27: }
28: }
如上面的代码片断所示,我们利用WebHostBuilder创建了一个WebHost对象。在此之前,我们调用扩展方法UseKestrel注册了一个KestrelServer类型的服务器,指定一个空的Action<IApplicationBuilder>对象作为参数调用了它的Configure方法,我们只得到这样的方法调用会创建了一个DelegateStartup对象。我们直接利用ConfigureServices方法得到所有自动注册的服务,并打印出每个服务的注册类型、生命周期模式和实现类型。当我们运行这个程序之后,控制台上将打印出如下图所示的服务列表。对于列出的这些服务,我们是不是看到很多熟悉的身影?

手工注册的服务
如果具体的项目需要采用依赖注入的方式来完成一些业务功能的实现,那就需要在应用初始化的过程中手工注册相应的服务。初次之外,我们也可以采用手工注册服务的方式来覆盖系统自动注册的服务。总的来说,我们可以采用种方式实现对服务的手工注册,其中一种就是按照如下的形式调用WebHostBuilder的ConfigureServices方法来注册服务,而另一种则是将服务注册实现在启动类的ConfigureServices方法中。
注册方式1:
1: new WebHostBuilder()
2: .ConfigureServices(svcs => svcs
3: .AddTransient<IFoo, Foo>()
4: .AddScoped<IBar, IBar>()
5: .AddSingleton<IBaz, Baz>())
6: …
注册方式2:
1: public class Startup
2: {
3: public void ConfigureServices(IServiceCollection svcs)
4: {
5: svcs.AddTransient<IFoo, Foo>()
6: .AddScoped<IBar, IBar>()
7: .AddSingleton<IBaz, Baz>();
8: }
9: …
10: }
通过前面的介绍,我们知道这两种方式真正执行服务注册的时机是不同的。第一种形式的服务注册发生在WebHostBuilder创建WebHost之前,包含这些服务的ServiceCollection以及由此创建的ServiceProvider将直接提供给后续创建的WebHost。而第二种形式的服务注册则发生在WebHost初始化过程中,实际上是借助一个ConventionBasedStartup对象来完成的。
二、以注入的形式提取服务
依赖注入的最终目录在于实现以注入的形式来消费预先注册的服务。在一个ASP.NET Core应用中,我们在很多地方都可以采用这种编程方式,我们在前一章中对此也有所提及。经过我的总结,我们常用的依赖注入编程主要应用在如下几个方面:
- 启动类型的构造函数和Configure方法中定义相应参数以注入的形式获取注册的服务。
- 中间件类型的构造函数和Invoke方法定义任何参数以注入的形式获取注册的服务。
- ASP.NET Core MVC应用中Controller类型的构造函数中定义任何参数以注入的形式获取注册的服务。
- ASP.NET Core MVC应用的View中通过@inject指令直接获取注册的服务。
启动类型的构造函数和Configure方法种注入服务
当我们在定义启动类型的时候,通过调用WebHostBuilder的ConfigureServices方法注册的服务可以在启动类的构造函数中进行注入,而启动类的Configure方法不但可以注入调用WebHostBuilder的ConfigureServices方法注册的服务,也可以注入自身ConfigureServices方法注册的服务。如下所示的代码片断展示了一个比较典型的例子。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>())
6: .UseStartup<Startup>()
7: …
8:
9: public class Startup
10: {
11: public Startup(IFoo foo, IBar bar)
12: {
13: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
14: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
15: }
16:
17: public void ConfigureServices(IServiceCollection svcs)
18: {
19: svcs.AddTransient<IBaz, Baz>()
20: .AddTransient<IGux, Gux>();
21: }
22:
23: public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz, IGux gux)
24: {
25: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
26: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
27: Debug.Assert(typeof(Baz).IsInstanceOfType(baz));
28: Debug.Assert(typeof(Gux).IsInstanceOfType(gux));
29: }
30: }
中间件类型的构造函数和Invoke方法中注入服务
当我们按照约定定义中间件类型的时候,我们可以在构造函数定义相应的参数来注入通过任何形式注册的服务。如下面的代码片断所示,中间件类型的构造函数和Invoke方法都定义了相应的参数来以注入的形式和获取通过调用WebHostBuilder的ConfigureServices方法注册的两个服务。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>())
6: .Configure(app=>app.UseMiddleware<FoobarMiddleware>())
7: ...
8:
9: public class FoobarMiddleware
10: {
11: private RequestDelegate _next;
12: public FoobarMiddleware(RequestDelegate next, IFoo foo, IBar bar)
13: {
14: _next = next;
15: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
16: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
17: }
18:
19: public async Task Invoke(HttpContext context, IFoo foo, IBar bar)
20: {
21: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
22: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
23: await _next(context);
24: }
25: }
Controller类型的构造函数中注入服务
在ASP.NET Core MVC应用中,我们经常在Controller类型的构造函数定义相应的参数来以注入的方式获取预先注册的服务。如下所示的这个HomeController就采用构造器注入的方式获取通过调用WebHostBuilder的ConfigureServices方法注册的两个服务。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>()
6: .AddMvc())
7: .Configure(app => app.UseMvc())
8: ...
9:
10: public class HomeController
11: {
12: public HomeController(IFoo foo, IBar bar)
13: {
14: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
15: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
16: }
17: ...
18: }
View中注入服务
如果我们在ASP.NET Core MVC应用的View中以注入的方式进行服务消费,我们有两种解决方案。第一种方案就是先按照上面这种方式将服务注入到Controller中,在将注入的服务通过ViewData或者ViewBag传递到View。另一种方式就是按照如下的方式直接使用@inject指令将注入的服务定义成当前View类型的属性。
1: new WebHostBuilder()
2: .UseKestrel()
3: .UseContentRoot(Directory.GetCurrentDirectory())
4: .ConfigureServices(svcs => svcs
5: .AddSingleton<IFoo, Foo>()
6: .AddSingleton<IBar, Bar>()
7: .AddMvc())
8: .Configure(app => app.UseMvc())
9: ...
10:
11: @using System.Reflection
12: @using System.Diagnostics
13: @inject IFoo Foo
14: @inject IBar Baz
15: @{
16: Debug.Assert(typeof(Foo).IsInstanceOfType(this.Foo));
17: Debug.Assert(typeof(Bar).IsInstanceOfType(this.Bar));
18: }
三、与第三方DI框架的整合
我们知道启动类型的ConfigureServices方法是可以返回一个ServiceProvider对象的,并且这个对象将直接作为WebHost的Services属性,成为一个全局单例的服务提供者。这个特性可以帮助我们实现与第三方DI框架的整合(比如Castle、Ninject、Autofac等)。在这里我不想“节外生枝”地引入某一个DI框架,而是自行创建一个简单的DI容器来演示这个主题。这个DI容器通过如下所示的Cat类型(这么名字来源于“机器猫”),它直接实现了IServiceProvider接口,所以一个Cat对象同时也是一个ServiceProvider对象。
1: public class Cat : IServiceProvider
2: {
3: private static readonly Cat _instance = new Cat();
4: private ConcurrentDictionary<Type, Func<Cat, object>> _registrations = new ConcurrentDictionary<Type, Func<Cat, object>>();
5: private IServiceProvider _backup;
6:
7: private Cat()
8: {
9: _backup = new ServiceCollection().BuildServiceProvider();
10: }
11:
12: public static Cat Instance
13: {
14: get { return _instance; }
15: }
16:
17: public Cat Register(IServiceCollection svcs)
18: {
19: _backup = svcs.BuildServiceProvider();
20: return this;
21: }
22:
23: public Cat Register(Type serviceType, Func<Cat, object> instanceAccessor)
24: {
25: _registrations[serviceType] = instanceAccessor;
26: return this;
27: }
28:
29: public object GetService(Type serviceType)
30: {
31: Func<Cat, object> instanceAccessor;
32: return _registrations.TryGetValue(serviceType, out instanceAccessor)? instanceAccessor(this): _backup.GetService(serviceType);
33: }
34: }
如上面的代码片断所示,Cat具有一个类型为ConcurrentDictionary<Type, Func<Cat, object>>类型的字段(_registrations)用来保存注册的服务,而服务的注册体现为服务类型与一个提供服务实例的委托对对象的映射,该映射通过调用第一个Register方法重载进行注册。除此之外,我还为这个类型定义了一个IServiceProvider接口类型的字段(_backup),如果实现的GetService方法不能根据指定的服务类型找到一个对应的Func<Cat, object>对象来提供服务对象,它将使用这个作为“后备”的ServiceProvider来提供这个服务。我们采用单例模式来使用Cat,这个单例对象通过只读属性Instance返回。
针对Cat这个DI容器的整体体现在如下这段程序中。如下面的代码片段所示,我们一共注册了三个服务,其中针对IFoo接口的服务直接注册在Cat单例对象上,针对IBar接口的服务通过调用ConfigureServices方法注册到WebHostBuilder上,而针对IBaz接口的服务则通过启动类的ConfiguresServices进行注册。值得注意的是,启动类的ConfigureServices方法返回的ServiceProvider正是这个Cat单例对象,在这之前我们调用它的Register方法将当前的ServiceCollection进行了注册。
1: public class Program
2: {
3: public static void Main()
4: {
5: Cat.Instance.Register(typeof(IFoo), _ => new Foo());
6: new WebHostBuilder()
7: .UseKestrel()
8: .ConfigureServices(svcs => svcs.AddSingleton<IBar, Bar>())
9: .UseStartup<Startup>()
10: .Build()
11: .Run();
12: }
13: }
14:
15: public class Startup
16: {
17: public IServiceProvider ConfigureServices(IServiceCollection svcs)
18: {
19: return Cat.Instance.Register(svcs.AddSingleton<IBaz, Baz>());
20: }
21:
22: public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz)
23: {
24: app.Run(async context =>{
25: context.Response.ContentType = "text/html";
26: await context.Response.WriteAsync($"IFoo => {foo.GetType().Name}<br/>");
27: await context.Response.WriteAsync($"IBar => {bar.GetType().Name}<br/>");
28: await context.Response.WriteAsync($"IBaz => {baz.GetType().Name}<br/>");
29: });
30: }
31: }
我们为启动类的Configure方法定了三个参数以注入的形式获取预先注册的这三个服务对象,并利用注册的中间件将服务的接口类型和真实类型之间的映射作为了响应的内容。我们启动应用并利用浏览器访问目标地址,这个类型映射关系将会按照如图5所示的形式出现在浏览器上。

微信公众账号:大内老A
历数依赖注入的N种玩法的更多相关文章
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- 【第1期】腾讯云的1001种玩法征集,Ipad mini和Kindle 等你拿!(文章评审中)
版权声明:本文由阁主的小跟班原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/695994001482226944 来源:腾云 ...
- ASP.NET MVC中使用Unity进行依赖注入的三种方式
在ASP.NET MVC中使用Unity进行依赖注入的三种方式 2013-12-15 21:07 by 小白哥哥, 146 阅读, 0 评论, 收藏, 编辑 在ASP.NET MVC4中,为了在解开C ...
- 实战Excel Add-in的三种玩法
作者:陈希章 发表于 2017年11月26日 前言 这个系列文章应该有一阵子没有更新了,原因是一如既往的多,但是根本所在是我对于某些章节其实还没有完全想好怎么写,尤其是对于Office Add-in这 ...
- 【腾讯云的1001种玩法】几种在腾讯云建立WordPress的方法(Linux)(二)
版权声明:本文由张宁原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/126547001488207964 来源:腾云阁 ht ...
- 小黄人IP营销的四种玩法思维导图
小黄人IP营销的四种玩法思维导图 ------------------------------ 本人微信公众帐号: 心禅道(xinchandao) 本人微信公众帐号:双色球预测合买(ssqyuce)
- 峰Spring4学习(2)依赖注入的几种方式
一.装配一个bean 二.依赖注入的几种方式 com.cy.entity People.java: package com.cy.entity; public class People { pri ...
- [ssh]记ssh的几种玩法
得到一台Linux的服务器,我们可以进行以下几种玩法: 先讲一讲几个参数: -f 要求 ssh在执行命令前退至后台.它用于当ssh准备询问口令或密语,但是用户希望它在后台进行.该选项隐含了-n选 ...
- 【SSH进阶之路】Spring的IOC逐层深入——依赖注入的两种实现类型(四)
上篇博文,我们介绍了为什么使用IOC容器,和IOC的设计思想以及IOC容器的优缺点,并且给大家转载了一篇介绍IOC原理的博文,我们这篇主要给大家依赖注入的两种方式,以及他们的优缺点. 我们这篇博文还是 ...
随机推荐
- JS发送跨域Post请求出现两次请求的解决办法
原文地址: http://www.cnblogs.com/JimmyBright/p/7681097.html 所有跨域的js在提交post请求的时候,如果服务端设置了可跨域访问 public sta ...
- 洛谷 P1070 道路游戏 解题报告
P1070 道路游戏 题目描述 小新正在玩一个简单的电脑游戏. 游戏中有一条环形马路,马路上有\(n\)个机器人工厂,两个相邻机器人工厂之间由一小段马路连接.小新以某个机器人工厂为起点,按顺时针顺序依 ...
- 51nod 1450 闯关游戏
首先肯定要先把所有的关卡打通后去找两星几率最大的关卡刷星(论打游戏经验的重要性). 所以从两星几率小的关打起,记录当前拿到x个星星的几率和当前走过的期望步数,如果发现剩下的关必须全两星,就直接计算答案 ...
- SP422 TRANSP2 - Transposing is Even More Fun——置换群+反演
挺神仙的置换题 SP422 TRANSP2 - Transposing is Even More Fun 这个博客除了开始举例子别的都是对的: https://blog.csdn.net/Braket ...
- 【HDU1710】树的遍历
题目大意:给定一棵 N 个节点的二叉树的前序遍历和中序遍历,求其后序遍历. 题解:递归操作,每次只需知道先序遍历和中序遍历的开始点,左子树大小即可,根据前序遍历的开始位置可知子树根节点的坐标,再在中序 ...
- laravel 命令行测试 Uncaught ReflectionException: Class config does not exist
require __DIR__ . '/vendor/autoload.php'; $app = require_once __DIR__ . '/bootstrap/app.php'; config ...
- SQL提高查询效率【in、not in、between、like】等条件讲述
在使用SQL语句查询数据库记录时,如果要查询相同的内容,有着不同的多种方法. 仍然,尽管使用多种方法可以得到相同的结果,但是,如果您使用不同的方法,在执行效益上是截然不同的.因此,我们得仔细考虑,如果 ...
- webpack全局引入库
我们在日常开发的时候会遇到一些每个页面都可能会引用到的库(例如jquery) 这可能会导致我们在每一个页面都需要写这样一个语句: import $ from 'jquery'; 可能有人会担心,最后打 ...
- python学习笔记5--json处理
import json #json串就是字符串. d = { 'car':{'color':'red','price':100,'count':50}, 'bus':{'color':'red','p ...
- html5 canvas loading(这可怕的编辑器,自动把我的canvas转义了)---以前收藏的整理了一下
/* super inefficient right now, could be improved */ var c = document.getElementById('canvasload'), ...