我们知道依赖注入(DI)是一种实现对象及其协作者或依赖关系之间松散耦合的技术。 ASP.NET Core包含一个简单的内建容器来支持构造器注入。

我们试图将DI的最佳实践带到.NET Core应用程序中,这表现在以下方面:

  1. 构造器注入
  2. 注册组件
  3. DI in testing

构造器注入

我们可以通过方法注入、属性注入、构造器注入的方式来注入具体的实例,一般来说构造器注入的方式被认为是最好的方式,所以在应用程序中将使用构造器注入,请避免使用别的注入方式。一个构造器注入的例子如:

  1. public class CharacterRepository : ICharacterRepository
  2. {
  3. private readonly ApplicationDbContext _dbContext;
  4. public CharacterRepository(ApplicationDbContext dbContext)
  5. {
  6. _dbContext = dbContext;
  7. }
  8. }

注册组件到容器

在使用DI之前,需要告诉容器组件之间的对应关系,例如:

  1. container.Register<IAService, AService>();

所以当你使用构造器注入的时候,你告诉构造函数需要注入IAService类型的实例,容器会根据你之前注册的对应关系创建AService的实例。

看起来一切都很简单,但在实际应用过程中并没有这么简单,试想在一个项目中,组件有成千上万个,这成千上万个组件之间的对应关系怎么样维护?

一个稍微改进点的策略根据这些组件的职责分类,把某一类组件的对应关系抽取成方法:

  1. private void RegisterApplicationServices(Container container)
  2. {
  3. container.Register<IAApplicationService, AApplicationService>();
  4. container.Register<IBApplicationService, BApplicationService>();
  5. //...
  6. }
  7. private void RegisterDomainServices(Container container)
  8. {
  9. container.Register<IADomainService, ADomainService>();
  10. container.Register<IBDomainService, BDomainService>();
  11. //...
  12. }
  13. private void RegisterOtherServices(Container container)
  14. {
  15. container.Register<IDataTimeSource, DataTimeSource>();
  16. container.Register<IUserFetcher, UserFetcher>();
  17. //...
  18. }

这两个分类有什么特点呢?第一个方法试图把所有的ApplicationService的组件对应关系汇总在一起,第二个方法试图把所有的DomainService的组件对应关系汇总在一起,比起之前已经有了很大的进步。不过随着组件的增加,你需要不断修改这几个方法。

基于公共接口来注册组件

第一个方法已经找到了同一类的组件,既然这些组件的性质是一样的,就可以用同样的接口来表示,定义一个空接口用来表示ApplicationService:

  1. public interface IApplicationService {}
  2. public interface IAApplicationService : IApplicationService { //.. }
  3. public interface IBApplicationService : IApplicationService { //.. }

一旦这些组件有了公共特点,尝试创建下面的扩展:

  1. container.Register(Classes.FromAssembly().BaseOn<IApplicationService>()
  2. .WithDefaultInterface());

这句代码的意思是显而易见的,扫描某个程序集,找到所有实现了IApplicationService的类进而把组件的对照关系注册到了容器中。

当组件拥有多个接口

类是可以拥有多个接口的,在实际开发中,这样的设计也是很常见的:

  1. public interface IOptions { //... }
  2. public interface IAlipayOptions : IOptions { //... }
  3. public class AlipayOptions: IAlipayOptions { //... }

利用上面介绍的扩展注册所有Options:

  1. container.Register(Classes.FromAssembly().BaseOn<IOptions>()
  2. .WithDefaultInterface());

尝试通过下面的构造器注入:

  1. public AlipayPayment(IAlipayOptions alipayOptions) { //... }

工作的很好,没有问题。但是当我们试图从容器里拿到所有的IOptions类型:

  1. container.ResolveAll<IOptions>();

你得不到任何IOptions类型的实例,原因在于向容器注册对应关系的过程是一对一的,我们之前的扩展.WithDefaultInterface()只注册了AlipayOptions和IAlipayOptions的关系,如果想通过上面的方式拿到所有继承了IOptions的实例,则需要使用另一个扩展:

  1. container.Register(Classes.FromAssembly().BaseOn<IOptions>()
  2. .WithAllInterfaces());

把注册文件放在正确的位置

我们通过分层的方式隔离了不同职责的程序集,最终Web/API项目将会引用这些低层的程序集。要想把 Web/API启动起来,需要把所有程序集定义的组件注册在Web/API项目的容器中。我们把Web/API这种能够启动的程序集叫做客户端。

所以一个典型的客户端需要通过下面代码来注册DI容器:

  1. container.Register(Classes.FromAssembly().BaseOn<IApplicationService>()
  2. .WithDefaultInterface());
  3. container.Register(Classes.FromAssembly().BaseOn<IDomainService>()
  4. .WithDefaultInterface());
  5. //...
  6. // 还有其他无法用公共接口表示的组件,这些组件可能来自于低层服务
  7. container.Register<IDateTimeSource, DateTimeSource>();
  8. container.Register<IUserFetcher, UserFetcher>();
  9. //...

这段代码描述了一个现象,Web/API客户端对低层的组件对应关系一清二楚,违反了Tell, Don't Ask Priciple. 正确的做法是:

Web/API客户端告诉低层组件,帮我安装你所在的程序集中所有的组件对应关系。

  1. // 安装所有
  2. services.Install(FromAssembly.Contains<IApplicationService>());
  3. services.Install(FromAssembly.Contains<IDomainService>());
  4. services.Install(FromAssembly.Contains<IOtherService>());

具体的组件对应关系应该定义在相应的程序集中。

这一节的思想都来源于Windsor Castle

DI in testing

人们在不断讨论单元测试的各种风格和差异,类似于通过Mock来管理依赖的单元测试被认为是一种反模式。见:To Kill a Mockingtest, 而DI的另一个功能在于便于写出有价值和有效的单元测试。

当你选择测试一个组件时,实际上要花很多的时间来准备依赖数据,这是显而易见的,因为组件并不是独立存在的。试想如果你能从容器中拿到这个组件,容器就会将所有的依赖关系创建好。

但是问题来了,比如说你的被测试组件依赖了一个能够给第三方发送请求的组件,这显然并不是你所期望的,你只需要注册一个假的事先准备好的组件即可。

对ApplicationServiceTests的组件注册如下:

  1. container.Install(FromAssembly.Contains<FakedComponentsInstaller>());
  2. //..Register other components that ApplicationService depend on

一个对SearchService的测试如下:

  1. [Fact]
  2. public async void WhenInputDataIsValidShouldGetSearchResult()
  3. {
  4. //Arrage
  5. var searchService = _container.Resolve<ISearchService>();
  6. var searchModel = SearchModelBuilder.Default().Build();
  7. //Act
  8. var result = await searchService.Search(searchModel);
  9. //Assert
  10. result.Count.Should().BeGreaterThan(0);
  11. }

Dependency injection in .NET Core的最佳实践的更多相关文章

  1. 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生

    [转].NET(C#):浅谈程序集清单资源和RESX资源   目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...

  2. 关于单元测试的思考--Asp.Net Core单元测试最佳实践

    在我们码字过程中,单元测试是必不可少的.但在从业过程中,很多开发者却对单元测试望而却步.有些时候并不是不想写,而是常常会碰到下面这些问题,让开发者放下了码字的脚步: 这个类初始数据太麻烦,你看:new ...

  3. asp.net core 系列之Dependency injection(依赖注入)

    这篇文章主要讲解asp.net core 依赖注入的一些内容. ASP.NET Core支持依赖注入.这是一种在类和其依赖之间实现控制反转的一种技术(IOC). 一.依赖注入概述 1.原始的代码 依赖 ...

  4. ASP.NET Core 依赖注入最佳实践与技巧

    ASP.NET Core 依赖注入最佳实践与技巧 原文地址:https://medium.com/volosoft/asp-net-core-dependency-injection-best-pra ...

  5. ASP.NET Core依赖注入最佳实践,提示&技巧

    分享翻译一篇Abp框架作者(Halil İbrahim Kalkan)关于ASP.NET Core依赖注入的博文. 在本文中,我将分享我在ASP.NET Core应用程序中使用依赖注入的经验和建议. ...

  6. EntityFramework Core进行读写分离最佳实践方式,了解一下(二)?

    前言 写过上一篇关于EF Core中读写分离最佳实践方式后,虽然在一定程度上改善了问题,但是在评论中有的指出更换到从数据库,那么接下来要进行插入此时又要切换到主数据库,同时有的指出是否可以进行底层无感 ...

  7. EntityFramework Core进行读写分离最佳实践方式,了解一下(一)?

    前言 本来打算写ASP.NET Core MVC基础系列内容,看到有园友提出如何实现读写分离,这个问题提的好,大多数情况下,对于园友在评论中提出的问题,如果是值得深究或者大多数同行比较关注的问题我都会 ...

  8. .NET Core 2.1中的HttpClientFactory最佳实践

    ASP.NET Core 2.1中出现一个新的HttpClientFactory功能, 它有助于解决开发人员在使用HttpClient实例从其应用程序发出外部Web请求时可能遇到的一些常见问题. 介绍 ...

  9. .NET Core中使用Dapper操作Oracle存储过程最佳实践

    为什么说是最佳实践呢?因为在实际开发中踩坑了,而且发现网上大多数文章给出的解决方法都不能很好地解决问题.尤其是在获取类型为OracleDbType.RefCursor,输出为:ParameterDir ...

随机推荐

  1. Acoustic modelling from the signal domain using CNNs

    3. Neural network architecture 此处描述了在本文当中所使用的网络结构,和所提取的关键特征(key features).首先,描述了两个新型的网络结构:the networ ...

  2. Python类——面向对象

    一.有关面向对象的一些知识 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发“更快更好更强...” ...

  3. Linux 第十四天

    6)Bash常用快捷键 快捷键 作用 ctr1+ a 把光标移动到命令行开头.如果我们输入的命令过长,想要把光标移| 动到命令行开头时使用. ctr1+e 把光标移动到命令行结尾. ctr1+c 强制 ...

  4. beego笔记

    beego学习笔记一:创建第一个beego Web项目 Go语言beego框架快速搭建体验五分钟讲解01 beego框架图文简介五分钟讲解02 beego框架图文简介五分钟讲解03-go语言简单方式操 ...

  5. s6-8 TCP 拥塞控制

    TCP 拥塞控制  虽然网络层也试图管理拥塞,但是,大多数繁重的任务是由TCP来完成的,因为针对拥塞的真正解决方案是减慢数据率  分组守恒:当有一个老的分组离开之后才允许新的分组注入网络  TC ...

  6. $(document).on('click','.classname',function(){}); VS $('.classname').on('click',function(){});

    jquery中用on来绑定事件,经常的写法有$(document).on('click','.classname',function(){});$('.classname').on('click',f ...

  7. kali入门

    第一章:入门kalilinux By:鬼尘 第一章基本上就是涵盖以下的主题: ·kali的发展简史 ·kali的一般用途 ·kali的下载与安装 ·kali的配置与更新 在本章结尾部分,我们还会介绍k ...

  8. spark wordcount 编程模型详解

    spark wordcount中一共经历多少个RDD?以及RDD提供的toDebugString    在控制台输入spark-shell   系统会默认创建一个SparkContext   sc h ...

  9. Objective-C iOS纯代码布局 一堆代码可以放这里!

    前言: 最近写的文章都是创业类,好吧,今天好好写写技术类的文章! 不过分享的不是IOS相关的文章,毕竟这几天在速成IOS,看的是objective-c,由于速成的很快,好累! 好在现在基本已经入了点门 ...

  10. Android开发之如何避免ANR(Keeping Your App Responsive)

    一:什么是ANR 如果应用程序不能响应用户的输入了,那么就可以说应用ANR了. 如果需要运行一个耗时较长的操作的时候,不要把这个任务放在UI线程上运行,而是单独创建一个线程运行那些操作. 以下情况会出 ...