ASP.NET Core:依赖注入
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:依赖注入的更多相关文章
- # ASP.NET Core依赖注入解读&使用Autofac替代实现
标签: 依赖注入 Autofac ASPNETCore ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Aut ...
- 实现BUG自动检测 - ASP.NET Core依赖注入
我个人比较懒,能自动做的事绝不手动做,最近在用ASP.NET Core写一个项目,过程中会积累一些方便的工具类或框架,分享出来欢迎大家点评. 如果以后有时间的话,我打算写一个系列的[实现BUG自动检测 ...
- [译]ASP.NET Core依赖注入深入讨论
原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog 这篇文章我们来深入探讨ASP.NET Core.MVC Core中 ...
- asp.net core 依赖注入几种常见情况
先读一篇注入入门 全面理解 ASP.NET Core 依赖注入, 学习一下基本使用 然后学习一招, 不使用接口规范, 直接写功能类, 一般情况下可以用来做单例. 参考https://www.cnblo ...
- ASP.NET Core依赖注入——依赖注入最佳实践
在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用 ...
- 自动化CodeReview - ASP.NET Core依赖注入
自动化CodeReview系列目录 自动化CodeReview - ASP.NET Core依赖注入 自动化CodeReview - ASP.NET Core请求参数验证 我个人比较懒,能自动做的事绝 ...
- ASP.NET Core 依赖注入最佳实践——提示与技巧
在这篇文章,我将分享一些在ASP.NET Core程序中使用依赖注入的个人经验和建议.这些原则背后的动机如下: 高效地设计服务和它们的依赖. 预防多线程问题. 预防内存泄漏. 预防潜在的BUG. 这篇 ...
- ASP.NET Core依赖注入最佳实践,提示&技巧
分享翻译一篇Abp框架作者(Halil İbrahim Kalkan)关于ASP.NET Core依赖注入的博文. 在本文中,我将分享我在ASP.NET Core应用程序中使用依赖注入的经验和建议. ...
- ASP.NET Core依赖注入解读&使用Autofac替代实现【转载】
ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Autofac实现和自定义实现扩展方法 3.1 安装Autof ...
- ASP.NET Core 依赖注入基本用法
ASP.NET Core 依赖注入 ASP.NET Core从框架层对依赖注入提供支持.也就是说,如果你不了解依赖注入,将很难适应 ASP.NET Core的开发模式.本文将介绍依赖注入的基本概念,并 ...
随机推荐
- 没事就要多做多练,Shell脚本循环例题做一做
Shell脚本循环例题 一.示例1 二.示例2 三.示例3 四.示例4 ...
- python基础之while语句操作
# i = 0# while (i < 9):# print("i ----> ",i)# i = i + 1# print(i,"i即将大于或者等于9,wh ...
- 开源协同办公平台部署教程:O2OA PAAS平台部署
一.镜像制作1.将安装介质o2server-5.0.3-linux.zip上传至镜像制作服务器上.(上传目录为/paas/xxhpaas/moka/o2oa)2.使用unzip命令解压安装包,参考命令 ...
- Splay与FHQ-Treap
两个一起学的,就放一块了. 主要是用来存板子. Splay //This is a Splay Tree. #include <cstdio> #include <cstring&g ...
- informix常见问题
1.中文乱码 https://www.cnblogs.com/equation/p/5545967.html 2.informix创建数据库和用户 https://wenku.baidu.com/vi ...
- Clip Studio Paint EX 1.10.6安装破解教程
clip studio paint是一款牛逼的绘图软件,简称csp.做动漫.漫画设计的同学的必备神器.本文教大家如何安装并破解 clip studio paint ex 1.10.6版本,文章教程提供 ...
- 构建前端第2篇之--ESLint 配置
张艳涛 写于2021-1-19 报错: http://eslint.org/docs/rules/space-before-function-paren Missing space before fu ...
- phpmyadmin error:#2002 - 服务器没有响应 (或者本地 MySQL 服务器的套接字没有正确配置)
1. 将 "phpMyAdmin/libraries"文件夹下的config.default.php文件中的$cfg['Servers'][$i]['host'] = 'local ...
- 题解 P6892 [ICPC2014 WF]Baggage
解题思路 非常好的一道构造题. 在手动模拟几个样例(也许不止几个)之后呢. 就可以发现其实这些操作的开始以及最后几步是有相通之处的. 关于手动模拟的样例放在了文章末尾,需要的自取. 先考虑操作次数. ...
- anyRTC SDK 5月迭代:优化自定义加密功能,让通信更安全
anyRTC SDK 5月上新,新增多种加密类型,让实时音视频通信更安全:新增移动端推流支持1080P分辨率的支持:此外还对事件上报.日志详情.数据统计.网络传输等多项功能进行了优化改进. 以下为更新 ...