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

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

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

构造器注入

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

public class CharacterRepository : ICharacterRepository
{
private readonly ApplicationDbContext _dbContext; public CharacterRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
}

注册组件到容器

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

container.Register<IAService, AService>();

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

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

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

private void RegisterApplicationServices(Container container)
{
container.Register<IAApplicationService, AApplicationService>();
container.Register<IBApplicationService, BApplicationService>();
//...
} private void RegisterDomainServices(Container container)
{
container.Register<IADomainService, ADomainService>();
container.Register<IBDomainService, BDomainService>();
//...
} private void RegisterOtherServices(Container container)
{
container.Register<IDataTimeSource, DataTimeSource>();
container.Register<IUserFetcher, UserFetcher>();
//...
}

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

基于公共接口来注册组件

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

public interface IApplicationService {}
public interface IAApplicationService : IApplicationService { //.. }
public interface IBApplicationService : IApplicationService { //.. }

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

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

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

当组件拥有多个接口

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

public interface IOptions { //... }
public interface IAlipayOptions : IOptions { //... }
public class AlipayOptions: IAlipayOptions { //... }

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

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

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

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

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

container.ResolveAll<IOptions>();

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

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

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

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

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

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

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

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

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

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

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

DI in testing

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

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

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

对ApplicationServiceTests的组件注册如下:

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

一个对SearchService的测试如下:

[Fact]
public async void WhenInputDataIsValidShouldGetSearchResult()
{
//Arrage
var searchService = _container.Resolve<ISearchService>();
var searchModel = SearchModelBuilder.Default().Build(); //Act
var result = await searchService.Search(searchModel); //Assert
result.Count.Should().BeGreaterThan(0);
}

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. Python类——面向对象

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

  2. 88、const、static、extern介绍

    一.const与宏的区别 const简介:之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量. 执行时刻:宏是预编译(编译之前处理)const是编译阶段. 编译检 ...

  3. spring的一个小例子(二)--解析前面的小例子

    接上篇:http://www.cnblogs.com/xuejupo/p/5236448.html 首先应该明白,一个web项目,web.xml是入口. 然后下面来分析上篇博客中出现的web.xml: ...

  4. JS prototype chaining(原型链)整理中······

    初学原型链整理 构造器(constructor).原型(prototype).实例(instance); 每一个构造器都有一个prototype对象,这个prototype对象有一个指针指向该构造器: ...

  5. vue组件通信新姿势

    在vue项目实际开发中我们经常会使用props和emit来进行子父组件的传值通信,父组件向子组件传递数据是通过prop传递的, 子组件传递数据给父组件是通过$emit触发事件来做到的.例如: Vue. ...

  6. python的基本数据类型(一)

    一.运算符逻辑运算and:两边都真则真or:有真则真not:非假是真 顺序:()>==>not==>and>=or 二.while.. elsewhile 条件: 代码块els ...

  7. react整理一二(初入React世界)

    react重要思想:组件化(按钮组件.对话框组件.日期组件等等).JSX语法 react把代码和正式渲染目标隔离开来,除了可以在浏览器渲染到DOM来开发网页,还能用于开发原生移动运用 1.专注视图层: ...

  8. mybatis 中的缓冲

    package com.oaec.mybatis.test; import com.oaec.mybatis.dao.StudentDao;import com.oaec.mybatis.entity ...

  9. 英语演讲稿——Get Along with Fear

    Hi. My name is Zhang Meng. I’m an engineer at Keysight. Today I’m not going to introduce my birthpla ...

  10. ASP.NET Core 微服务初探[2]:熔断降级之Polly

    当我们从单体架构迁移到微服务模式时,其中一个比较大的变化就是模块(业务,服务等)间的调用方式.在以前,一个业务流程的执行在一个进程中就完成了,但是在微服务模式下可能会分散到2到10个,甚至更多的机器( ...