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

一、什么是依赖注入

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

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

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

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

二、使用框架提供的服务

Startup类中的ConfigureServices方法负责定义应用程序将使用的服务,包括平台功能,比如EntityFramework Core和ASP.NET Core MVC。最初,IServiceCollection只向ConfigureServices提供了几个服务定义。如下面的例子:

除了使用默认提供的几个服务定义,我们还可以自己添加。下面是一个如何使用一些扩展方法(如AddDbContext,AddIdentity)向容器中添加额外服务的例子:

public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服务
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});
// 添加MVC服务
services.AddControllersWithViews();
}

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

当然,除了使用各种框架功能配置应用程序外,还可以使用ConfigureServices来配置自己的应用程序服务。

三、注册服务

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

// 添加自己的服务
// IRepository是一个接口,表示要请求的类型
// UserRepository表示IRepository接口的具体实现类型
services.AddTransient<IRepository, UserRepository>();

每个services.Add<service>调用添加服务。例如,services.AddControllersWithViews()表示添加MVC需要的服务。

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

using System.Linq;
using DependencyInjectionDemo.Model;
using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionDemo.Controllers
{
public class CharactersController : Controller
{
// 定义私有的只读字段
private readonly ICharacterRepository _characterRepository; /// <summary>
/// 通过构造函数注入并且给私有字段赋值
/// </summary>
/// <param name="characterRepository"></param>
public CharactersController(ICharacterRepository characterRepository)
{
_characterRepository = characterRepository;
} public IActionResult Index()
{
return View();
} private void PopulateCharactersIfNoneExist()
{
// 如果不存在则添加
if(!_characterRepository.ListAll().Any())
{
_characterRepository.Add(new Character("Tom"));
_characterRepository.Add(new Character("Jack"));
_characterRepository.Add(new Character("Kevin"));
}
}
}
}

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

using DependencyInjectionDemo.Model;
using System.Collections.Generic; namespace DependencyInjectionDemo.Repository
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
int Add(Character character);
}
}

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

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

using DependencyInjectionDemo.Context;
using DependencyInjectionDemo.Model;
using System.Collections.Generic;
using System.Linq; namespace DependencyInjectionDemo.Repository
{
public class CharacterRepository : ICharacterRepository
{
// 定义私有字段
private readonly AppDbContext _dbContext; /// <summary>
/// 通过构造函数注入,并且给私有字段赋值
/// </summary>
/// <param name="dbContext"></param>
public CharacterRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
} public int Add(Character character)
{
// 添加
_dbContext.Characters.Add(character);
// 保存
return _dbContext.SaveChanges();
} public IEnumerable<Character> ListAll()
{
return _dbContext.Characters.AsEnumerable();
}
}
}

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

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

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

public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服务
// 这里是注册AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
}); // 添加自己的服务
// IRepository是一个接口,表示要请求的类型
// UserRepository表示IRepository接口的具体实现类型
services.AddTransient<IRepository, UserRepository>(); // 注册ICharacterRepository类型
services.AddTransient<ICharacterRepository, CharacterRepository>();
// 添加MVC服务
services.AddControllersWithViews();
}

Entity Framework Core的数据上下文应当使用Scope的生命周期添加到服务容器中。如果使用上面的AddDbContext<T>方法则会自动处理。仓储将使用与Entity Framework Core相同的生命周期。

四、生命周期

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

  • Transient:瞬时生命周期。瞬时生命周期服务在它们每次请求时被创建。这一生命周期适合轻量级的、无状态的服务。
  • Scoped:作用域生命周期。作用域生命周期服务在每次请求时被创建一次。
  • Singleton:单例生命周期。单例生命周期服务在它们第一次被请求时创建,并且每个后续请求将使用相同的实例。如果你的应用程序需要单例行为,则建议让服务容器管理服务的生命周期,而不是在自己的类中实现单例模式和管理对象的生命周期。

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

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

using System;

namespace DependencyInjectionDemo.Repository
{
/// <summary>
/// 基接口
/// </summary>
public interface IOperationRepository
{
Guid GetOperationId();
} /// <summary>
/// 瞬时接口
/// </summary>
public interface IOperationTransientRepository: IOperationRepository
{ } /// <summary>
/// 作用域接口
/// </summary>
public interface IOperationScopeRepository : IOperationRepository
{ } /// <summary>
/// 单例接口
/// </summary>
public interface IOperationSingletonRepository : IOperationRepository
{ }
}

我们使用OperationRepository类来实现这些接口:

using System;

namespace DependencyInjectionDemo.Repository
{
public class OperationRepository : IOperationRepository
{
private readonly Guid _guid; public OperationRepository()
{
_guid = Guid.NewGuid();
} public Guid GetOperationId()
{
return _guid;
}
} public class OperationTransientRepository : OperationRepository, IOperationTransientRepository
{ } public class OperationScopeRepository : OperationRepository, IOperationScopeRepository
{ } public class OperationSingletonRepository : OperationRepository, IOperationSingletonRepository
{ }
}

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

public void ConfigureServices(IServiceCollection services)
{
// 添加EntityFrameworkCore服务
// 这里是注册AppDbContext使用AddDbContext<T>的形式
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
}); // 添加自己的服务
// IRepository是一个接口,表示要请求的类型
// UserRepository表示IRepository接口的具体实现类型
services.AddTransient<IRepository, UserRepository>(); // 注册ICharacterRepository类型
services.AddTransient<ICharacterRepository, CharacterRepository>(); // 添加瞬时生命周期
services.AddTransient<IOperationTransientRepository, OperationTransientRepository>();
// 添加作用域生命周期
services.AddScoped<IOperationScopeRepository, OperationScopeRepository>();
// 添加单例生命周期
services.AddSingleton<IOperationSingletonRepository, OperationSingletonRepository>();
// 添加MVC服务
services.AddControllersWithViews();
}

然后添加一个控制器:

using DependencyInjectionDemo.Repository;
using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionDemo.Controllers
{
public class OperationController : Controller
{
// 定义私有字段
private readonly IOperationTransientRepository _transientRepository;
private readonly IOperationScopeRepository _scopeRepository;
private readonly IOperationSingletonRepository _singletonRepository; /// <summary>
/// 通过构造函数实现注入
/// </summary>
/// <param name="transientRepository"></param>
/// <param name="scopeRepository"></param>
/// <param name="singletonRepository"></param>
public OperationController(IOperationTransientRepository transientRepository,
IOperationScopeRepository scopeRepository,
IOperationSingletonRepository singletonRepository)
{
_transientRepository = transientRepository;
_scopeRepository = scopeRepository;
_singletonRepository = singletonRepository;
} public IActionResult Index()
{
// ViewBag赋值
ViewBag.TransientGuid = _transientRepository.GetOperationId();
ViewBag.ScopedGuid = _scopeRepository.GetOperationId();
ViewBag.SingletonGuid = _singletonRepository.GetOperationId();
return View();
}
}
}

对应的Index视图代码:

<div class="row">
<div>
<h2>GuidItem Shows</h2> <h3>TransientGuid: @ViewBag.TransientGuid</h3> <h3>ScopedGuid: @ViewBag.ScopedGuid</h3> <h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
</div>
</div>

然后我们打开两个浏览器,刷新多次,只会发现“TransientGuid” 和“ScopedGuid”的值在不断变化,而“SingletonGuid”的值是不会变化的,这就体现了单例模式的作用,如下图所示:

但是这样还不够,要知道我们的Scoped的解读是“生命周期横贯整次请求”,但是现在演示起来和Transient好像没有什么区别(因为两个页面每次浏览器请求仍然是独立的,并不包含于一次中),所以我们采用以下代码来演示下(同一请求源):

@*引入命名空间*@
@using DependencyInjectionDemo.Repository @*通过该inject引入*@
@inject IOperationTransientRepository OperationTransientRepository
@inject IOperationScopeRepository OperationScopeRepository
@inject IOperationSingletonRepository OperationSingletonRepository <div class="row">
<div>
<h2>GuidItem Shows</h2>
<h3>TransientGuid: @OperationTransientRepository.GetOperationId()</h3>
<h3>ScopedGuid: @OperationScopeRepository.GetOperationId()</h3>
<h3>SingletonGuid: @OperationSingletonRepository.GetOperationId()</h3>
</div>
</div>

然后修改Index视图:

<div class="row">
<div>
@Html.Partial("GuidPartial")
<h2>**************************</h2>
<h2>GuidItem Shows</h2>
<h3>TransientGuid: @ViewBag.TransientGuid</h3>
<h3>ScopedGuid: @ViewBag.ScopedGuid</h3>
<h3>SingletonGuid: @ViewBag.SingletonGuid</h3>
</div>
</div>

在运行程序执行:

可以看到:每次请求的时候Scope生命周期在同一请求中是不变的,而Transient生命周期还是会不断变化的。

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

五、请求服务

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

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

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

六、设计你的依赖服务

应该设计你的依赖注入服务来获取它们的合作者。这意味着在你的服务中,避免使用有状态的静态方法调用和直接实例化依赖的类型。

如果你的类有太多的依赖关系被注入时该怎么办?这通常表明你的类试图做太多,并且可能违反了单一职责原则。看看是否可以通过转移一些职责到一个新的类来重构。

注意,你的Controller类应该重点关注用户界面(UI),因此业务规则和数据访问实现细节应该保存在这些适合单独关注的类中。

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

GitHub示例代码:git@github.com:jxl1024/DependencyInjection.git

ASP.NET Core:依赖注入的更多相关文章

  1. # ASP.NET Core依赖注入解读&使用Autofac替代实现

    标签: 依赖注入 Autofac ASPNETCore ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Aut ...

  2. 实现BUG自动检测 - ASP.NET Core依赖注入

    我个人比较懒,能自动做的事绝不手动做,最近在用ASP.NET Core写一个项目,过程中会积累一些方便的工具类或框架,分享出来欢迎大家点评. 如果以后有时间的话,我打算写一个系列的[实现BUG自动检测 ...

  3. [译]ASP.NET Core依赖注入深入讨论

    原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog 这篇文章我们来深入探讨ASP.NET Core.MVC Core中 ...

  4. asp.net core 依赖注入几种常见情况

    先读一篇注入入门 全面理解 ASP.NET Core 依赖注入, 学习一下基本使用 然后学习一招, 不使用接口规范, 直接写功能类, 一般情况下可以用来做单例. 参考https://www.cnblo ...

  5. ASP.NET Core依赖注入——依赖注入最佳实践

    在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用 ...

  6. 自动化CodeReview - ASP.NET Core依赖注入

    自动化CodeReview系列目录 自动化CodeReview - ASP.NET Core依赖注入 自动化CodeReview - ASP.NET Core请求参数验证 我个人比较懒,能自动做的事绝 ...

  7. ASP.NET Core 依赖注入最佳实践——提示与技巧

    在这篇文章,我将分享一些在ASP.NET Core程序中使用依赖注入的个人经验和建议.这些原则背后的动机如下: 高效地设计服务和它们的依赖. 预防多线程问题. 预防内存泄漏. 预防潜在的BUG. 这篇 ...

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

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

  9. ASP.NET Core依赖注入解读&使用Autofac替代实现【转载】

    ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Autofac实现和自定义实现扩展方法 3.1 安装Autof ...

  10. ASP.NET Core 依赖注入基本用法

    ASP.NET Core 依赖注入 ASP.NET Core从框架层对依赖注入提供支持.也就是说,如果你不了解依赖注入,将很难适应 ASP.NET Core的开发模式.本文将介绍依赖注入的基本概念,并 ...

随机推荐

  1. Luogu P2754 星际转移问题

    Luogu P2754 星际转移问题 思路 首先,对于地球能否到达月球的问题,考虑使用并查集维护. 对于每艘飞船能够到达的站点,放进一个集合里,若两艘飞船的集合有交集,那么就合并两个集合,最后只要地球 ...

  2. MQTT 2——服务端安装与客户端测试

    本篇记录一下MQTT服务器,客户端,JAVA客户端的选择开发与测试 MQTT服务端选择与安装过程:MQTT客户端测试工具安装与测试:MQTT JAVA客户端的选择与开发,测试 MQTT服务器选择与安装 ...

  3. ZooKeeper 分布式锁 Curator 源码 03:可重入锁并发加锁

    前言 在了解了加锁和锁重入之后,最需要了解的还是在分布式场景下或者多线程并发加锁是如何处理的? 并发加锁 先来看结果,在多线程对 /locks/lock_01 加锁时,是在后面又创建了新的临时节点. ...

  4. java并发编程基础——线程通信

    线程通信 当线程在系统内运行时,程序通常无法准确的控制线程的轮换执行,但我们可以通过一些机制来保障线程的协调运行 一.传统的线程通信 传统的线程通信主要是通过Object类提供的wait(),noti ...

  5. kubernetes/k8s CSI分析-容器存储接口分析

    更多 k8s CSI 的分析,可以查看这篇博客kubernetes ceph-csi分析,以 ceph-csi 为例,做了详细的源码分析. 概述 kubernetes的设计初衷是支持可插拔架构,从而利 ...

  6. 达梦数据库(DM8)大规模并行集群MPP 2节点安装部署

    达梦数据库大规模并行集群MPP 2节点安装部署   1.环境准备   os 数据库版本 ip mpp角色 centos7.x86 DM8 192.168.30.100 mpp1 centos7.x86 ...

  7. AI开发者十问:10分钟了解AI开发的基本过程

    摘要:从AI开发模型.框架.工具,到提升开发效率的学习办法,为AI开发者逐一解答. 本文分享自华为云社区<10分钟了解AI开发的基本过程>,作者:简单坚持. 1.AI开发究竟在开发什么? ...

  8. 深入刨析tomcat 之---第15篇 对应20章, myAdmin案例代码

    writedby 张艳涛 有没有和我一样做到第20章的?今天基本结束了本书的阅读.最后一个案例没有demo,那我写了一回,如果有需要的可以在本地试试 路径 D:\wksp_study\designbo ...

  9. 一文带你搞定AOP切面

    摘要:AOP在spring中又叫"面向切面编程",是对传统我们面向对象编程的一个补充,主要操作对象就是"切面",可以简单的理解它是贯穿于方法之中,在方法执行前. ...

  10. Jenkins(8080)未授权访问

    下载地址http://mirrors.jenkins.io/debian/ 测试 浏览器访问http://localhost:8080/manage可以直接访问 点击脚本命令行 println &qu ...