理解ASP.NET Core - [03] Dependency Injection
注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录
依赖注入
什么是依赖注入
简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。
依赖注入有什么好处
依赖注入在.NET中,可谓是“一等公民”,处处都离不开它,那么它有什么好处呢?
假设有一个日志类 FileLogger,用于将日志记录到本地文件。
public class FileLogger
{
public void LogInfo(string message)
{
}
}
日志很常用,几乎所有服务都需要记录日志。如果不使用依赖注入,那么我们就必须在每个服务中手动 new FileLogger 来创建一个 FileLogger 实例。
public class MyService
{
private readonly FileLogger _logger = new FileLogger();
public void Get()
{
_logger.LogInfo("MyService.Get");
}
}
如果某一天,想要替换掉 FileLogger,而是使用 ElkLogger,通过ELK来处理日志,那么我们就需要将所有服务中的代码都要改成 new ElkLogger。
public class MyService
{
private readonly ElkLogger _logger = new ElkLogger();
public void Get()
{
_logger.LogInfo("MyService.Get");
}
}
- 在一个大型项目中,这样的代码分散在项目各处,涉及到的服务均需要进行修改,显然一个一个去修改不现实,且违反了“开闭原则”。
- 如果Logger中还需要其他一些依赖项,那么用到Logger的服务也要为其提供依赖,如果依赖项修改了,其他服务也必须要进行更改,更加增大了维护难度。
- 很难进行单元测试,因为它无法进行 mock
正因如此,所以依赖注入解决了这些棘手的问题:
- 通过接口或基类(包含抽象方法或虚方法等)将依赖关系进行抽象化
- 将依赖关系存放到服务容器中
- 由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中
ASP.NET Core内置的依赖注入
服务生存周期
Transient
瞬时,即每次获取,都是一个全新的服务实例
Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。
Singleton
单例,即在单个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。
服务释放
若服务实现了IDisposable接口,并且该服务是由DI容器创建的,那么你不应该去Dispose,DI容器会对服务自动进行释放。
如,有Service1、Service2、Service3、Service4四个服务,并且都实现了IDisposable接口,如:
public class Service1 : IDisposable
{
public void Dispose()
{
Console.WriteLine("Service1.Dispose");
}
}
public class Service2 : IDisposable
{
public void Dispose()
{
Console.WriteLine("Service2.Dispose");
}
}
public class Service3 : IDisposable
{
public void Dispose()
{
Console.WriteLine("Service3.Dispose");
}
}
public class Service4 : IDisposable
{
public void Dispose()
{
Console.WriteLine("Service4.Dispose");
}
}
并注册为:
public void ConfigureServices(IServiceCollection services)
{
// 每次使用完(请求结束时)即释放
services.AddTransient<Service1>();
// 超出范围(请求结束时)则释放
services.AddScoped<Service2>();
// 程序停止时释放
services.AddSingleton<Service3>();
// 程序停止时释放
services.AddSingleton(sp => new Service4());
}
构造函数注入一下
public ValuesController(
Service1 service1,
Service2 service2,
Service3 service3,
Service4 service4)
{ }
请求一下,获取输出:
Service2.Dispose
Service1.Dispose
这些服务实例都是由DI容器创建的,所以DI容器也会负责服务实例的释放和销毁。注意,单例此时还没到释放的时候。
但如果注册为:
public void ConfigureServices(IServiceCollection services)
{
// 注意与上面的区别,这个是直接 new 的,而上面是通过 sp => new 的
services.AddSingleton(new Service1());
services.AddSingleton(new Service2());
services.AddSingleton(new Service3());
services.AddSingleton(new Service4());
}
此时,实例都是咱们自己创建的,DI容器就不会负责去释放和销毁了,这些工作都需要我们开发人员自己去做。
更多注册方式,请参考官方文档-Service registration methods
TryAdd{Lifetime}扩展方法
当你将同样的服务注册了多次时,如:
services.AddSingleton<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();
那么当使用IEnumerable<{Service}>(下面会讲到)解析服务时,就会产生多个MyService实例的副本。
为此,框架提供了TryAdd{Lifetime}扩展方法,位于命名空间Microsoft.Extensions.DependencyInjection.Extensions下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。
services.AddTransient<IMyService, MyService1>();
// 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关)
services.TryAddTransient<IMyService, MyService1>();
services.TryAddTransient<IMyService, MyService2>();
- TryAdd:通过参数
ServiceDescriptor将服务类型、实现类型、生命周期等信息传入进去 - TryAddTransient:对应AddTransient
- TryAddScoped:对应AddScoped
- TryAddSingleton:对应AddSingleton
- TryAddEnumerable:这个和
TryAdd的区别是,TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:
// 注册了 IMyService - MyService1
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
// 注册了 IMyService - MyService2
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>());
// 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
解析同一服务的多个不同实现
默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。
如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable<{Service}>来解析(顺序同注册顺序一致):
public interface IAnimalService { }
public class DogService : IAnimalService { }
public class PigService : IAnimalService { }
public class CatService : IAnimalService { }
public void ConfigureServices(IServiceCollection services)
{
// 生命周期没有限制
services.AddTransient<IAnimalService, DogService>();
services.AddScoped<IAnimalService, PigService>();
services.AddSingleton<IAnimalService, CatService>();
}
public ValuesController(
// CatService
IAnimalService animalService,
// DogService、PigService、CatService
IEnumerable<IAnimalService> animalServices)
{
}
Replace && Remove 扩展方法
上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用Replace和Remove了
// 将 IMyService 的实现替换为 MyService1
services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 注册的实现 MyService
services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 的所有注册
services.RemoveAll<IMyService>();
// 清除所有服务注册
services.Clear();
Autofac
Autofac 是一个老牌DI组件了,接下来我们使用Autofac替换ASP.NET Core自带的DI容器。
- 安装nuget包:
Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection
- 替换服务提供器工厂
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
// 通过此处将默认服务提供器工厂替换为 autofac
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
- 在 Startup 类中添加 ConfigureContainer 方法
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public ILifetimeScope AutofacContainer { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
// 1. 不要 build 或返回任何 IServiceProvider,否则会导致 ConfigureContainer 方法不被调用。
// 2. 不要创建 ContainerBuilder,也不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
// 3. 你仍然可以在此处通过微软默认的方式进行服务注册
services.AddOptions();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" });
});
}
// 1. ConfigureContainer 用于使用 Autofac 进行服务注册
// 2. 该方法在 ConfigureServices 之后运行,所以这里的注册会覆盖之前的注册
// 3. 不要 build 容器,不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
public void ConfigureContainer(ContainerBuilder builder)
{
// 将服务注册划分为模块,进行注册
builder.RegisterModule(new AutofacModule());
}
public class AutofacModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
// 在此处进行服务注册
builder.RegisterType<UserService>().As<IUserService>();
}
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
// 通过此方法获取 autofac 的 DI容器
AutofacContainer = app.ApplicationServices.GetAutofacRoot();
}
}
服务解析和注入
上面我们主要讲了服务的注入方式,接下来看看服务的解析方式。解析方式有两种:
- IServiceProvider
- ActivatorUtilities
- 用于创建未在DI容器中注册的服务实例
- 用于某些框架级别的功能
构造函数注入
上面我们举得很多例子都是使用了构造函数注入——通过构造函数接收参数。构造函数注入是非常常见的服务注入方式,也是首选方式,这要求:
- 构造函数可以接收非依赖注入的参数,但必须提供默认值
- 当服务通过
IServiceProvider解析时,要求构造函数必须是public - 当服务通过
ActivatorUtilities解析时,要求构造函数必须是public,虽然支持构造函数重载,但必须只能有一个是有效的,即参数能够全部通过依赖注入得到值
方法注入
顾名思义,方法注入就是通过方法参数来接收服务实例。
[HttpGet]
public string Get([FromServices]IMyService myService)
{
return "Ok";
}
属性注入
ASP.NET Core内置的依赖注入是不支持属性注入的。但是Autofac支持,用法如下:
老规矩,先定义服务和实现
public interface IUserService
{
string Get();
}
public class UserService : IUserService
{
public string Get()
{
return "User";
}
}
然后注册服务
- 默认情况下,控制器的构造函数参数由DI容器来管理吗,而控制器实例本身却是由ASP.NET Core框架来管理,所以这样“属性注入”是无法生效的
- 通过
AddControllersAsServices方法,将控制器交给 autofac 容器来处理,这样就可以使“属性注入”生效了
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddControllersAsServices();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterModule<AutofacModule>();
}
public class AutofacModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<UserService>().As<IUserService>();
var controllerTypes = Assembly.GetExecutingAssembly().GetExportedTypes()
.Where(type => typeof(ControllerBase).IsAssignableFrom(type))
.ToArray();
// 配置所有控制器均支持属性注入
builder.RegisterTypes(controllerTypes).PropertiesAutowired();
}
}
最后,我们在控制器中通过属性来接收服务实例
public class ValuesController : ControllerBase
{
public IUserService UserService { get; set; }
[HttpGet]
public string Get()
{
return UserService.Get();
}
}
通过调用Get接口,我们就可以得到IUserService的实例,从而得到响应
User
一些注意事项
- 避免使用服务定位模式。尽量避免使用
GetService来获取服务实例,而应该使用DI。
using Microsoft.Extensions.DependencyInjection;
public class ValuesController : ControllerBase
{
private readonly IServiceProvider _serviceProvider;
// 应通过依赖注入的方式获取服务实例
public ValuesController(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
[HttpGet]
public string Get()
{
// 尽量避免通过 GetService 方法获取服务实例
var myService = _serviceProvider.GetService<IMyService>();
return "Ok";
}
}
- 避免在
ConfigureServices中调用BuildServiceProvider。因为这会导致创建第二个DI容器的副本,从而导致注册的单例服务出现多个副本。
public void ConfigureServices(IServiceCollection services)
{
// 不要在该方法中调用该方法
var serviceProvider = services.BuildServiceProvider();
}
一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:
- 在 Scoped 或 Transient 服务中解析 Singleton 服务
- 在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)
当在
Development环境中运行、并通过CreateDefaultBuilder生成主机时,默认的服务提供程序会进行如下检查:- 不能在根服务提供程序解析 Scoped 服务,这会导致 Scoped 服务的生命周期提升为 Singleton,因为根容器在应用关闭时才会释放。
- 不能将 Scoped 服务注入到 Singleton 服务中
随着业务增长,需要依赖注入的服务也越来越多,建议使用扩展方法,封装服务注入,命名为
Add{Group_Name},如将所有 AppService 的服务注册封装起来
namespace Microsoft.Extensions.DependencyInjection
{
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplicationService(this IServiceCollection services)
{
services.AddTransient<Service1>();
services.AddScoped<Service2>();
services.AddSingleton<Service3>();
services.AddSingleton(sp => new Service4());
return services;
}
}
}
然后在ConfigureServices中调用即可
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationService();
}
框架默认提供的服务
以下列出一些常用的框架已经默认注册的服务:
| 服务类型 | 生命周期 |
|---|---|
| Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Transient |
| IHostApplicationLifetime | Singleton |
| IHostLifetime | Singleton |
| IWebHostEnvironment | Singleton |
| IHostEnvironment | Singleton |
| Microsoft.AspNetCore.Hosting.IStartup | Singleton |
| Microsoft.AspNetCore.Hosting.IStartupFilter | Transient |
| Microsoft.AspNetCore.Hosting.Server.IServer | Singleton |
| Microsoft.AspNetCore.Http.IHttpContextFactory | Transient |
| Microsoft.Extensions.Logging.ILogger | Singleton |
| Microsoft.Extensions.Logging.ILoggerFactory | Singleton |
| Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Singleton |
| Microsoft.Extensions.Options.IConfigureOptions | Transient |
| Microsoft.Extensions.Options.IOptions | Singleton |
| System.Diagnostics.DiagnosticSource | Singleton |
| System.Diagnostics.DiagnosticListener | Singleton |
理解ASP.NET Core - [03] Dependency Injection的更多相关文章
- ASP.NET Core 的 Dependency Injection
ASP.NET Core使用了大量的DI(Dependency Injection)设计,有用过Autofac或类似的DI Framework对此应该不陌生.本篇将介绍ASP.NET Core的依赖注 ...
- 目录-理解ASP.NET Core
<理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...
- 理解ASP.NET Core - [04] Host
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...
- 理解依赖注入(DI - Dependency Injection)
系列教程 Spring 框架介绍 Spring 框架模块 Spring开发环境搭建(Eclipse) 创建一个简单的Spring应用 Spring 控制反转容器(Inversion of Contro ...
- 理解 ASP.NET Core: 处理管道
理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...
- 理解ASP.NET Core - [01] Startup
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...
- 理解ASP.NET Core - [02] Middleware
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 中间件 先借用微软官方文档的一张图: 可以看到,中间件实际上是一种配置在HTTP请求管道中,用 ...
- 理解ASP.NET Core - 配置(Configuration)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...
- 理解ASP.NET Core - 选项(Options)
注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...
随机推荐
- Java文件I/O简单介绍
目录 一.File类 1.1 构造方法 1.2 常用方法 1.3 例子 二.基础I/O:字节流.字符流 2.1 字节流 2.1.1 字节输出流 OutputStream 2.1.2 FileOutpu ...
- jstack 命令使用经验总结
jstack 命令的基本使用 jstack 在命令使用上十分简洁, 其信息量与复杂度主要体如今 thread dump 内容的分析上;web # 最基本的使用sudo -u xxx jstack {v ...
- 《SEO实战密码》
一.搜索引擎 1.蜘蛛访问网站时都会先访问网站根目录下的 robots.txt 文件. 2.蜘蛛喜欢访问经常更新的页面. 3.离首页点击距离越近,页面权重越高. 4.使用"" + ...
- DC-4 靶机渗透测试
DC-4 渗透测试 冲冲冲,努力学习 .掌握 hydra ,nc反弹shell 记住你要干嘛, 找地方上传shell(大多以后台登录为切入点,再反弹shell),shell提权到root 操作机:ka ...
- YsoSerial 工具常用Payload分析之Common-Collections2、4(五)
前言 Common-Collections <= 3.2.1 对应与YsoSerial为CC1.3.5.6.7 ,Commno-collections4.0对应与CC2.4. 这篇文章结束官方原 ...
- 数据库比对工具SQL(表、字段、触发器、索引、视图、存储过程)
做一个数据库比对小工具,把SQL做一个笔记 SELECT object_id AS ID --表ID,'表' sType,Name --表名FROM sys.tablesORDER BY Name-- ...
- C++ //多态案例 -计算器类(普通写法 和 多态写法) //利用多态实现计算器 //多态好处: //1.组织结构清晰 //2.可读性强 //3.对于前期和后期扩展以及维护性高
1 //多态案例 -计算器类(普通写法 和 多态写法) 2 3 #include <iostream> 4 #include <string> 5 using namespac ...
- Map 与 unordered_map 横向与纵向测试,附带原始数据与测试程序
写程序时,面临用Map还是unordered_map,总是很纠结,于是写了个程序进行测试 Map 与 unordered_map 横向与纵向测试,附带原始数据与测试程序 简单数据(4 Byte) 首先 ...
- springboot的单元测试(总结两种)
.personSunflowerP { background: rgba(51, 153, 0, 0.66); border-bottom: 1px solid rgba(0, 102, 0, 1); ...
- 免费个人图床搭建gitee+PicGo
我们写博客的时候经常会需要配图,特别是markdown写的时候只能通过网络链接来展示图片. 首先来说存储仓库.测试过几款存储图片的仓库,最终选择方案3: 1.阿里OSS需要付费,空间和流量双向收费,对 ...