

ASP.NET Core - 依赖注入(一) - 啊晚
source link: https://www.cnblogs.com/wewant/p/17107546.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. Ioc 与 DI
Ioc 和DI 这两个词大家都应该比较熟悉,这两者已经在各种开发语言各种框架中普遍使用,成为框架中的一种基本设施了。
Ioc 是控制反转, Inversion of Control 的缩写,DI 是依赖注入,Inject Dependency 的缩写。
所谓控制反转,反转的是类与类之间依赖的创建。类型A依赖于类型B时,不依赖于具体的类型,而是依赖于抽象,不在类A中直接 new 类B的对象,而是通过外部传入依赖的类型对象,以实现类与类之间的解耦。所谓依赖注入,就是由一个外部容器对各种依赖类型对象进行管理,包括对象的创建、销毁、状态保持等,并在某一个类型需要使用其他类型对象的时候,由容器进行传入。
下图是一张网图,是关于Ioc解耦比较经典的图示过程了。至于依赖解耦的好处,就不在这里细讲了,如果有对依赖注入基本概念不理解的,可以稍微搜索一下相关的文章,也可以参考 ASP.NET Core 依赖注入 | Microsoft Learn 官方文档中的讲解。

Ioc是一种设计思想,而DI是这种思想的具体实现。依赖注入是一种设计模式,是对面向对象编程五大基本原则中的依赖倒置原则的实践,其中很重要的一个点就是 Ioc 容器的实现。
2. .NET Core 依赖注入的基本用法
在 .NET Core 平台下,有一套自带的轻量级Ioc框架,如果是ASP.NET Core项目,更是在使用主机的时候自动集成了进去,我们在startup类中的ConfigureServices方法中的代码就是往容器中配置依赖注入关系,如果是控制台项目的话,还需要自己去集成。除此之外,.NET 平台下还有一些第三方依赖注入框架,如 Autofac、Unity、Castle Windsor等。
这里先不讨论第三方框架的内容,先简单介绍一下.Net Core平台自带的Ioc框架的使用。
2.1 服务
依赖项注入术语中,服务通常是指向其他对象提供服务的对象,既可以作为其他类的依赖项,也可能依赖于其他服务。服务是Ioc容器管理的对象。
2.2 服务生命周期
使用了依赖注入框架之后,所有我们注入到容器中的类型的创建、销毁工作都由容器来完成,那么容器什么时候创建一个类型实例,什么时候销毁一个类型实例呢?这就涉及到注入服务的生命周期了。根据我们的需要,我们可以向容器中注册服务的时候,对服务的生命周期进行设置。服务的生命周期有以下三种:
(1) 单例 Singleton
注册成单例模式的服务,整个应用程序生命周期以内只创建一个实例。在应用内第一个使用到该服务时创建,在应用程序停止时销毁。
在某些情况下,对于某些特殊的类,我们需要注册成单例模式,这可以减少实例初始化的消耗,还能实现跨 Service 事务的功能。
(2)范围(或者作用域) Scoped
在同一个范围内只初始化一个实例 。在 web 应用中,可以理解为每一个 request 级别只创建一个实例,同一个 http request 会在一个 scope 内。
(3)多例 Tranisent
每一次使用到服务时都会创建一个新的实例,每一次对该依赖的获取都是一个新实例。
2.3 服务注册
在ASP.NET Core这样的web应用框架中,在使用主机的时候就自动集成了依赖注入框架,之后我们可以通过 IServiceCollection 对象来注册依赖注入关系。前面入口文件一篇讲过,.NET 6 之前可以在 Startup 类中的 ConfigureServices 方法中进行注册,该方法传入IServiceCollection参数,.NET 6 之后,可以通过 WebApplicationBuilder 对象的 Services属性进行注册。
服务注册常用的方法如下:
-
Add 方法
通过参数 ServiceDescriptor 将服务类型、实现类型、生命周期等信息传入进去,是服务注册最基本的方法。其中 ServiceDescriptor 参数又有多种变形。// 最基本的服务注册方法,除此之外还有其他各种变形 builder.Services.Add(new ServiceDescriptor(typeof(IRabbit), typeof(Rabbit), ServiceLifetime.Transient)); builder.Services.Add(ServiceDescriptor.Scoped<IRabbit, Rabbit>()); builder.Services.Add(ServiceDescriptor.Singleton(typeof(IRabbit), (services) => new Rabbit()));
-
Add{lifetime}扩展方法
基于 Add 方法的扩展方法,包括以下几种,每种都有多个重载:// 基于生命周期的扩展方法,以下为实例,正式开发中不可能将一个类型注册为多个生命周期,会抛出异常 builder.Services.AddTransient<IRabbit, Rabbit>(); builder.Services.AddTransient(typeof(IRabbit), typeof(Rabbit)); builder.Services.AddScoped<IRabbit, Rabbit>(); builder.Services.AddSingleton<IRabbit, Rabbit>();
-
TryAdd{lifetime}扩展方法
对于 Add{lifetime} 方法的扩展,位于命名空间 Microsoft.Extensions.DependencyInjection.Extensions 下。
与 Add{lifetime} 方法相比,差别在于当使用 Add{lifetime} 方法将同样的服务注册了多次时,在使用 IEnumerable<{Service}> 解析服务时,就会产生多个实例的副本,这可能会导致一些意料之外的 bug,特别是单例生命周期的服务。// 同一个服务同一个实现注入多次 builder.Services.AddSingleton<IRabbit, Rabbit>(); builder.Services.AddSingleton<IRabbit, Rabbit>(); [ApiController] [Route("[controller]")] public class InjectTestController : ControllerBase { private readonly IEnumerable<IRabbit> _rabbits; public InjectTestController(IEnumerable<IRabbit> rabbits) { _rabbits = rabbits; [HttpGet("")] public Task InjectTest() { // 2个IRabbit实例 Console.WriteLine(_rabbits.Count()); var rabbit1 = _rabbits.First(); var rabbit2 = _rabbits.ElementAt(1); // 都是 Rabbit 类型 Console.WriteLine(rabbit1 is Rabbit); Console.WriteLine(rabbit2 is Rabbit); // 两个实例不是同一个 Console.WriteLine(rabbit1 == rabbit2); return Task.CompletedTask; } }
调用接口后,打印输出结果如下:
而使用 TryAdd{lifetime} 方法,当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。
- TryAdd:对于 Add 方法
- TryAddTransient:对应AddTransient 方法
- TryAddScoped:对应AddScoped 方法
- TryAddSingleton:对应AddSingleton 方法
将服务注册改成以下代码:
builder.Services.AddTransient<IRabbit, Rabbit>(); // 由于上面已经注册了服务类型 IRabbit,所以下面的代码不不会执行任何操作(与生命周期无关) builder.Services.TryAddTransient<IRabbit, Rabbit>(); builder.Services.TryAddTransient<IRabbit, Rabbit1>();
在上面的控制器中新增以下方法:
[HttpGet(nameof(InjectTest1))] public Task InjectTest1() { // 只有1个IRabbit实例 Console.WriteLine(_rabbits.Count()); var rabbit1 = _rabbits.First(); // 都是 Rabbit 类型 Console.WriteLine(rabbit1 is Rabbit); return Task.CompletedTask; }
调用接口后,打印输出结果如下:
-
TryAddEnumerable 方法
与 TryAdd 对应,区别在于TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。
将服务注册改成以下代码:
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>()); builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit1>()); // 未进行任何操作,因为 IRabbit 服务的 Rabbit实现在上面已经注册了 builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IRabbit, Rabbit>());
在上面的控制器新增一个方法:
[HttpGet(nameof(InjectTest2))] public Task InjectTest2() { // 2个IRabbit实例 Console.WriteLine(_rabbits.Count()); var rabbit1 = _rabbits.First(); var rabbit2 = _rabbits.ElementAt(1); // 第一个是 Rabbit 类型,第二个是 Rabbit1类型 Console.WriteLine(rabbit1 is Rabbit); Console.WriteLine(rabbit2 is Rabbit1); return Task.CompletedTask; }
调用接口后,控制台打印如下:
-
Repalce 与 Remove 方法
当我们想要从Ioc容器中替换或是移除某些已经注册的服务时,可以使用Replace和Remove。
// 将容器中注册的IRabbit实现替换为 Rabbit1 builder.Services.Replace(ServiceDescriptor.Transient<IRabbit, Rabbit1>()); // 从容器中 IRabbit 注册的实现 Rabbit1 builder.Services.Remove(ServiceDescriptor.Transient<IRabbit, Rabbit1>()); // 移除 IRabbit服务的所有注册 builder.Services.RemoveAll<IRabbit>(); // 清空容器中的所有服务注册 builder.Services.Clear();
以上是 .NET Core 框架自带的 Ioc 容器的一些基本概念和依赖关系注入的介绍,下一章是注入到容器中的服务使用部分。
参考文章:
ASP.NET Core 依赖注入 | Microsoft Learn
理解ASP.NET Core - 依赖注入(Dependency Injection)
ASP.NET Core 系列:
目录:ASP.NET Core 系列总结
上一篇:ASP.NET Core - 自定义中间件
Recommend
-
7
1. IStartupFilter 上面讲到的方式虽然能够根据不同环境将Startup中的启动逻辑进行分离,但是有些时候我们还会可以根据应用中的功能点将将一系列相关中间件的注册封装到一起,从 Startup 类中分离,单独进行维护,以便更清晰地管理...
-
5
ASP.NET Core - .NET 6 的入口文件 自从.NET 6 开始,微...
-
6
1. 请求管道 请求管道是什么?请求管道描述的是一个请求进到我们的后端应用,后端应用如何处理的过程,从接收到请求,之后请求怎么流转,经过哪些处理,最后怎么返回响应。请求管道就是一次请求在后端应用的生命周期。了解请求管道,有助于...
-
7
ASP.NET Core - 自定义中间件 上一章讲了...
-
12
.NET Core 依赖注入的基本用法 话接上篇,这一章介绍 .NET Core 框架自带的轻量级 Ioc 容器下服务使用的一些知识点,大家可以先看看上一篇文章 [ASP.NET Core - 依赖注入(一)] 2.3 服务...
-
6
4. ASP.NET Core默认服务 之前讲了中间件,实际上一个中间件要正常进行工作,通常需要许多的服务配合进行,而中间件中的服务自然也是通过 Ioc 容器进行注册和注入的。前面也讲到,按照约定中间件的封装一般会提供一个 User{Midd...
-
11
一个应用要运行起来,往往需要读取很多的预设好的配置信息,根据约定好的信息或方式执行一定的行为。 配置的本质就是软件运行的参数,在一个软件实现中需要的参数非常多,如果我们以 Hard Code(硬编码)的方式写在应用代码中,这样配置就会很乱,而且后续也不...
-
10
2. 配置添加 配置系统可以读取到配置文件中的信息,那必然有某个地方可以将配置文件添加到配置系统中。之前的文章中讲到 ASP.NET Core 入口文件中,builder(WebApplicationBuilder 对象) 中有一个 Configuration 属性,这里就是我们扩展添...
-
9
就像 Web Api 接口可以对入参进行验证,避免用户传入非法的或者不符合我们预期的参数一样,选项也可以对配置源的内容进行验证,避免配置中的值与选项类中的属性不对应或者不满足预期,毕竟大部分配置都是通过字符串的方式,验证是很有必要的。
-
3
依赖注入实现了系统之间、模块之间和对象之间依赖关系的解耦,基本上是现代应用程序框架必不可少的一个组成部分。 ABP的依赖注入系统是基于Microsoft的依赖注入扩展库(Microsoft.Extensions.DependencyInjection),所以能够完全兼容.net Core...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK