原文:Dependency Injection

作者:Steve Smith

翻译:刘浩杨

校对:许登洋(Seay)高嵩

ASP.NET Core 的底层设计支持和使用依赖注入。ASP.NET Core 应用程序可以利用内置的框架服务将它们注入到启动类的方法中,并且应用程序服务能够配置注入。由 ASP.NET Core 提供的默认服务容器提供了最小功能集并且不是要取代其他容器。

查看或下载示例代码

什么是依赖注入

依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术。将类用来执行其操作(Action)的这些对象以某种方式提供给该类,而不是直接实例化合作者或使用静态引用。通常,类会通过它们的构造函数声明其依赖关系,允许它们遵循 显示依赖原则 (Explicit Dependencies Principle) 。这种方法被称为 “构造函数注入(constructor injection)”。

当类的设计使用 DI 思想,它们耦合更加松散,因为它们没有对它们的合作者直接硬编码的依赖。这遵循 依赖倒置原则(Dependency Inversion Principle),其中指出 “高层模块不应该依赖于低层模块;两者都应该依赖于抽象。” 类要求在它们构造时向其提供抽象(通常是 interfaces ),而不是引用特定的实现。提取接口的依赖关系和提供这些接口的实现作为参数也是 策略设计模式(Strategy design pattern) 的一个示例。

当系统被设计使用 DI ,很多类通过它们的构造函数(或属性)请求其依赖关系,有一个类被用来创建这些类及其相关的依赖关系是很有帮助的。这些类被称为 容器(containers) ,或者更具体地,控制反转(Inversion of Control,IoC) 容器或者依赖注入(Dependency injection,DI)容器。容器本质上是一个工厂,负责提供向它请求的类型实例。如果一个给定类型声明它具有依赖关系,并且容器已经被配置为提供依赖类型,它将把创建依赖关系作为创建请求实例的一部分。通过这种方式,可以向类型提供复杂的依赖关系而不需要任何硬编码的类型构造。除了创建对象的依赖关系,容器通常还会管理应用程序中对象的生命周期。

ASP.NET Core 包含了一个默认支持构造函数注入的简单内置容器(由 IServiceProvider 接口表示),并且 ASP.NET 使某些服务可以通过 DI 获取。ASP.NET 的容器指的是它管理的类型为 services。在这篇文章的其余部分, services 是指由 ASP.NET Core 的 IoC 容器管理的类型。你在应用程序 Startup 类的 ConfigureServices 方法中配置内置容器的服务。

注解

Martin Fowler 写过一篇全面的文章发表在 Inversion of Control Containers and the Dependency Injection Pattern. Microsoft 模式与实践小组(Microsoft Patterns and Practices)也有丰富的关于 Dependency Injection 的描述。

注解

本文介绍了依赖注入,因为它适用于所有的 ASP.NET 应用程序。 MVC 控制器中的依赖注入包含在 Dependency Injection and Controllers

使用框架提供的服务

Startup 类的 ConfigureServices 方法负责定义应用程序将使用的服务,包括平台功能,比如 Entity Framework Core 和 ASP.NET Core MVC 。最初, IServiceCollection 只向 ConfigureServices 提供了几个服务定义。下面是一个如何使用一些扩展方法(如 AddDbContextAddIdentity 和 AddMvc )向容器中添加额外服务的例子。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); services.AddMvc(); // Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}

ASP.NET 提供的功能和中间件,例如 MVC,遵循约定——使用一个单一的 AddService扩展方法来注册所有该功能所需的服务。

小技巧

你可以在 Startup 的方法中通过它们的参数列表请求一些框架提供的服务 - 查看 Application Startup 获取更多信息。

当然,除了使用各种框架功能配置应用程序,你也能够使用 ConfigureServices 配置你自己的应用程序服务。

注册你自己的服务

你可以按照如下方式注册你自己的应用程序服务。第一个泛型类型表示将要从容器中请求的类型(通常是一个接口)。第二个泛型类型表示将由容器实例化并且用于完成这些请求的具体类型。

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

注解

每个 services.Add<service> 调用添加(和可能配置)服务。 例如: services.AddMvc() 添加 MVC 需要的服务。

AddTransient 方法用于将抽象类型映射到为每一个需要它的对象分别实例化的具体服务。这被称作为服务的 生命周期(lifetime),另外的生命周期选项在下面描述。为你注册的每一个服务选择合适的生命周期是重要的。应该为每个请求的类提供一个新的服务实例?应该在一个给定的网络请求中使用一个实例?或者应该在应用程序生命周期中使用单例?

在这篇文章的示例中,有一个名称为 CharactersController 的简单控制器。它的 Index 方法显示已经存储在应用程序中的当前字符列表,并且,如果它不存在的话,初始化具有少量字符的集合。值得注意的是,虽然应用程序使用 Entity Framework Core 和 ApplicationDbContext 类作为持久化,这在控制器中都不是显而易见的。相反,具体的数据访问机制被抽象在遵循 仓储模式(repository pattern) 的 ICharacterRepository 接口后面。 ICharacterRepository 的实例是通过构造函数请求并分配给一个私有字段,然后用来访问所需的字符。

public class CharactersController : Controller
{
private readonly ICharacterRepository _characterRepository; public CharactersController(ICharacterRepository characterRepository)
{
_characterRepository = characterRepository;
} // GET: /characters/
public IActionResult Index()
{
PopulateCharactersIfNoneExist();
var characters = _characterRepository.ListAll(); return View(characters);
} private void PopulateCharactersIfNoneExist()
{
if (!_characterRepository.ListAll().Any())
{
_characterRepository.Add(new Character("Darth Maul"));
_characterRepository.Add(new Character("Darth Vader"));
_characterRepository.Add(new Character("Yoda"));
_characterRepository.Add(new Character("Mace Windu"));
}
}
}

ICharacterRepository 只定义了控制器需要使用 Character 实例的两个方法。

using System.Collections.Generic;
using DependencyInjectionSample.Models; namespace DependencyInjectionSample.Interfaces
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
void Add(Character character);
}
}

这个接口在运行时使用一个具体的 CharacterRepository 类型来实现。

注解

在 CharacterRepository 类中使用 DI 的方式是一个你可以在你的应用程序服务遵循的通用模型,不只是在“仓储”或者数据访问类中。

using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces; namespace DependencyInjectionSample.Models
{
public class CharacterRepository : ICharacterRepository
{
private readonly ApplicationDbContext _dbContext; public CharacterRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
} public IEnumerable<Character> ListAll()
{
return _dbContext.Characters.AsEnumerable();
} public void Add(Character character)
{
_dbContext.Characters.Add(character);
_dbContext.SaveChanges();
}
}
}

注意的是 CharacterRepository 需要一个 ApplicationDbContext 在它的构造函数中。依赖注入用于像这样的链式方法并不少见,每个请求依次请求它的依赖关系。容器负责解析所有的依赖关系,并返回完全解析后的服务。

注解

创建请求对象,和它需要的所有对象,以及那些需要的所有对象,有时称为一个 对象图(object graph)。同样的,必须解析依赖关系的集合通常称为 依赖树(dependency tree) 或者 依赖图(dependency graph)

在这种情况下, ICharacterRepository 和 ApplicationDbContext 都必须在 Startup 类ConfigureServices 方法的服务容器中注册。ApplicationDbContext 的配置调用 AddDbContext<T> 扩展方法。下面的代码展示 CharacterRepository 类型的注册。

public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase()
); // Add framework services.
services.AddMvc(); // Register application services.
services.AddScoped<ICharacterRepository, CharacterRepository>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();
}

Entity Framework 上下文应当使用 Scoped 生命周期添加到服务容器中。如果你使用上图所示的帮助方法则这是自动处理的。仓储将使 Entity Framework 使用相同的生命周期。

警告

最主要的危险是要小心从单例解析一个 Scoped 服务。在这种情况下很可能处理后续请求的时候服务会出现不正确的状态。

服务生命周期和注册选项

ASP.NET 服务可以被配置为以下生命周期:

瞬时

瞬时(Transient)生命周期服务在它们每次请求时被创建。这一生命周期适合轻量级的,无状态的服务。

作用域

作用域(Scoped)生命周期服务在每次请求被创建一次。

单例

单例(Singleton)生命周期服务在它们第一次被请求时创建(或者如果你在 ConfigureServices

运行时指定一个实例)并且每个后续请求将使用相同的实例。如果你的应用程序需要单例行为,建议让服务容器管理服务的生命周期而不是在自己的类中实现单例模式和管理对象的生命周期。

服务可以用多种方式在容器中注册。我们已经看到了如何通过指定具体类型用来注册一个给定类型的服务实现。除此之外,可以指定一个工厂,它将被用来创建需要的实例。第三种方式是直接指定要使用的类型的实例,在这种情况下容器将永远不会尝试创建一个实例。

为了说明这些生命周期和注册选项之间的差异,考虑一个简单的接口将一个或多个任务表示为有一个唯一标识符 OperationId 的 操作 。依据我们如何配置这个服务的生命周期,容器将为请求的类提供相同或不同的服务实例。要弄清楚哪一个生命周期被请求,我们将创建每一个生命周期选项的类型:

using System;

namespace DependencyInjectionSample.Interfaces
{
public interface IOperation
{
Guid OperationId { get; }
} public interface IOperationTransient : IOperation
{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}
}

我们使用 Operation 类实现这些接口。它的构造函数接收一个 Guid,若未提供则生成一个新的 Guid

接下来,在 ConfigureServices 中,每一个类型根据它们命名的生命周期被添加到容器中:

services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();

请注意, IOperationSingletonInstance 服务使用一个具有已知 Guid.Empty ID 的具体实例,所以该类型在使用时是明确的。我们还注册了一个依赖于其他每个 Operation 类型的 OperationService,因此在一个请求中对于每个操作类型,该服务获取相同的实例或创建一个新的实例作为控制器将是明确的。所有服务通过属性暴露依赖关系,因此它们可以显示在视图中。

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
public class OperationService
{
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; } public OperationService(IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}
}
}

为了证明对象的生命周期在应用程序的每个单独的请求内,还是请求之间,此示例包含OperationsController

 请求每一个 IOperation 类型和 OperationService。 Index action 接下来显示所有控制器和服务的 OperationId 值。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionSample.Controllers
{
public class OperationsController : Controller
{
private readonly OperationService _operationService;
private readonly IOperationTransient _transientOperation;
private readonly IOperationScoped _scopedOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationSingletonInstance _singletonInstanceOperation; public OperationsController(OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_operationService = operationService;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
_singletonInstanceOperation = singletonInstanceOperation;
} public IActionResult Index()
{
// viewbag contains controller-requested services
ViewBag.Transient = _transientOperation;
ViewBag.Scoped = _scopedOperation;
ViewBag.Singleton = _singletonOperation;
ViewBag.SingletonInstance = _singletonInstanceOperation; // operation service has its own requested services
ViewBag.Service = _operationService;
return View();
}
}
}

现在两个独立的请求到这个 controller action:



观察 OperationId 值在请求和请求之间的变化。

  • 瞬时(Transient) 对象总是不同的;向每一个控制器和每一个服务提供了一个新的实例
  • 作用域(Scoped) 对象在一次请求中是相同的,但在不同请求中是不同的
  • 单例(Singleton) 对象对每个对象和每个请求是相同的(无论是否在 ConfigureServices 中提供实例)

请求服务

来自 HttpContext 的一次 ASP.NET 请求中可用的服务通过 RequestServices 集合公开的。

请求服务将你配置的服务和请求描述为应用程序的一部分。当你的对象指定依赖关系,这些满足要求的对象通过查找 RequestServices 中对应的类型得到,而不是 ApplicationServices

通常,你不应该直接使用这些属性,而更倾向于通过类的构造函数请求需要的类的类型,并且让框架来注入依赖关系。这将会生成更易于测试的 (查看 Testing) 和更松散耦合的类。

注解

更倾向于请求依赖关系作为构造函数的参数来访问 RequestServices 集合。

设计你的依赖注入服务

你应该设计你的依赖注入服务来获取它们的合作者。这意味着在你的服务中避免使用有状态的静态方法调用(代码被称为 static cling)和直接实例化依赖的类型。当选择实例化一个类型还是通过依赖注入请求它时,它可以帮助记住这句话, New is Glue。通过遵循 面向对象设计的 SOLID 原则,你的类将倾向于小、易于分解及易于测试。

如果你发现你的类往往会有太多的依赖关系被注入时该怎么办?这通常表明你的类试图做太多,并且可能违反了单一职责原则(SRP) - 单一职责原则。看看你是否可以通过转移一些职责到一个新的类来重构类。请记住,你的 Controller 类应该重点关注用户界面(User Interface,UI),因此业务规则和数据访问实现细节应该保存在这些适合单独关注的类中。

关于数据访问,如果你已经在 Startup 类中配置了 EF,那么你能够方便的注入 Entity Framework 的 DbContext 类型到你的控制器中。然而,最好不要在你的 UI 项目直接依赖 DbContext。相反,依赖于一个抽象(比如一个仓储接口),并且限定使用 EF (或其他任何数据访问技术)来实现这个接口。这将减少应用程序和特定的数据访问策略之间的耦合,并且使你的应用程序代码更容易测试。

替换默认的服务容器

内置的服务容器的意图在于提供框架的基本需求并且大多数客户应用程序建立在它之上。然而,开发人员可以很容易地使用他们的首选容器替换默认容器。ConfigureServices 方法通常返回 void,但是如果改变它的签名返回 IServiceProvider,可以配置并返回一个不同的容器。有很多 IOC 容器可用于 .NET。在这个例子中, Autofac 包被使用。

首先,在 project.json 的 dependencies 属性中添加适当的容器包:

"dependencies" : {
"Autofac": "4.0.0-rc2-237",
"Autofac.Extensions.DependencyInjection": "4.0.0-rc2-200"
},

接着,在 ConfigureServices 中配置容器并返回 IServiceProvider

public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// add other framework services // Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<DefaultModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return container.Resolve<IServiceProvider>();
}

注解

当使用第三方 DI 容器时,你必须更改 ConfigureServices 让它返回 IServiceProvider 而不是 void

最后,在 DefaultModule 中配置 Autofac:

public class DefaultModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
}
}

在运行时,Autofac 将被用来解析类型和注入依赖关系。 了解更多有关使用 Autofac 和 ASP.NET Core

建议

当使用依赖注入时,请记住以下建议:

  • DI 针对具有复杂依赖关系的对象。控制器,服务,适配器和仓储都是可能被添加到 DI 的对象的例子。

    避免直接在 DI 中存储数据和配置。例如,用户的购物车通常不应该被添加到服务容器中。配置应该使用 Options Model。 同样, 避免 “数据持有者” 对象只是为了允许访问其他对象而存在。如果可能的话,最好是通过 DI 获取实际的项。
  • 避免静态访问服务。
  • 避免在应用程序代码中服务定位。
  • 避免静态访问 HttpContext

注解

像所有的建议,你可能遇到必须忽视其中一个的情况。我们发现了少见的例外 – 非常特别的情况是框架本身。

记住,依赖注入是静态/全局对象访问模式的 另一选择。如果你把它和静态对象访问混合的话,你将无法了解 DI 的有用之处。

附加资源

返回目录

ASP.NET Core 中文文档 第三章 原理(10)依赖注入的更多相关文章

  1. ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化

    原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...

  2. ASP.NET Core 中文文档 第三章 原理(1)应用程序启动

    原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...

  3. ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态

    原文:Managing Application State 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:高嵩 在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行 ...

  4. ASP.NET Core 中文文档 第三章 原理(2)中间件

    原文:Middleware 作者:Steve Smith.Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:许登洋(Seay) 章节: 什么是中间件 用 IApplicationBu ...

  5. ASP.NET Core 中文文档 第三章 原理(3)静态文件处理

    原文:Working with Static Files 作者:Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay).孟帅洋(书缘) 静态文 ...

  6. ASP.NET Core 中文文档 第三章 原理(11)在多个环境中工作

    原文: Working with Multiple Environments 作者: Steve Smith 翻译: 刘浩杨 校对: 孟帅洋(书缘) ASP.NET Core 介绍了支持在多个环境中管 ...

  7. ASP.NET Core 中文文档 第三章 原理(17)为你的服务器选择合适版本的.NET框架

    原文:Choosing the Right .NET For You on the Server 作者:Daniel Roth 翻译:王健 校对:谢炀(Kiler).何镇汐.许登洋(Seay).孟帅洋 ...

  8. ASP.NET Core 中文文档 第三章 原理(7)配置

    原文:Configuration 作者:Steve Smith.Daniel Roth 翻译:刘怡(AlexLEWIS) 校对:孟帅洋(书缘) ASP.NET Core 支持多种配置选项.应用程序配置 ...

  9. ASP.NET Core 中文文档 第三章 原理(8)日志

    原文:Logging 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:何镇汐.许登洋(Seay) ASP.NET Core 内建支持日志,也允许开发人员轻松切换为他们想用的其他日 ...

随机推荐

  1. 解决PHP-问题:Class 'SimpleXMLElement' not found in

    1.问题 在ubuntu 16.10中,学习PHP,学习到PHP如何生成XML文件时候,碰到了这个问题: PHP Fatal error: Class 'ClassName\SimpleXMLElem ...

  2. 【踩坑速记】二次依赖?android studio编译运行各种踩坑解决方案,杜绝弯路,总有你想要的~

    这篇博客,只是把自己在开发中经常遇到的打包编译问题以及解决方案给大家稍微分享一下,不求吸睛,但求有用. 1.大家都知道我们常常会遇到dex超出方法数的问题,所以很多人都会采用android.suppo ...

  3. Unity3D框架插件uFrame实践记录(二)

    5.创建属性和命令 本小节主要内容包括: 在Element节点上创建属性数据 在Element节点上创建命令数据 5.1.在Element节点上创建属性数据 在这里,我们首先为Login节点中的属性( ...

  4. 以向VS 程序打包集成自动写入注册表功能为例,介绍如何实现自由控制安装过程

    最近由于项目部署时需要更灵活的控制程序安装的流程以及自定义安装行为,特意研究了一下VS程序打包,把解决办法和大家分享一下. 以VS2010为例: 这是一个已经设置好最基本的Visual Studio ...

  5. AbpZero--1.如何开始

    1.加群 群号:104390185,下载这个文件并解压 用VS2015打开aspnet-zero-1.9.0.1 2.修改Web项目web.config连接字符串 <add name=" ...

  6. Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

    学习架构探险,从零开始写Java Web框架时,在学习到springAOP时遇到一个异常: "C:\Program Files\Java\jdk1.7.0_40\bin\java" ...

  7. iOS之应用版本号的设置规则

    版本号的格式:v<主版本号>.<副版本号>.<发布号>  版本号的初始值:v1.0.0 管理规则: 主版本号(Major version) 1.  产品的主体构件进 ...

  8. 编译器开发系列--Ocelot语言7.中间代码

    Ocelot的中间代码是仿照国外编译器相关图书Modern Compiler Implementation 中所使用的名为Tree 的中间代码设计的.顾名思义,Tree 是一种树形结构,其特征是简单, ...

  9. 在redis中使用lua脚本让你的灵活性提高5个逼格

    在redis的官网上洋洋洒洒的大概提供了200多个命令,貌似看起来很多,但是这些都是别人预先给你定义好的,但你却不能按照自己的意图进行定制, 所以是不是感觉自己还是有一种被束缚的感觉,有这个感觉就对了 ...

  10. windows 7(32/64位)GHO安装指南(U盘引导篇)~

    上一篇我们说了怎么制作U盘启动盘,那么这一篇让我们来看看如何进行正确的U盘引导启动. 现在的个人计算机一般分为台式机和笔记本,由于各厂商的喜好不同(开玩笑的啦),所以对于主板的BIOS设置各所不同.进 ...