依赖注入
首先理解"控制反转"的思想
它是把 "创建 / 管理依赖对象的控制权",从 "使用依赖的类内部" 转移到 "外部容器 / 框架"。
生活例子:你要实现吃饭的功能,自己来做饭吃,就相当于控制权在自己手里,要多辣自己都可以控制;而点外卖的话,控制权不在自己手里,称为"控制反转",具体的口味都是由商家决定的。
"依赖注入"是实现"控制反转"的方式
可以这样理解,依赖就是需要的东西,比如美食,将需要的东西注入进来,而不是自己去创建。需要什么直接去要,自己不用实现。
依赖注入代码示例:
先定义一个简单的日志服务:
cs
// 1. 服务接口(被依赖的对象)
public interface ILogService
{
void Log(string message); // 日志方法
}
// 2. 服务实现类
public class ConsoleLogService : ILogService
{
// 实现:控制台输出日志
public void Log(string message)
{
Console.WriteLine($"【控制台日志】{message}");
}
}
依赖注入实现:
cs
using System;
using Microsoft.Extensions.DependencyInjection; // .NET DI核心命名空间
// 业务类:依赖ILogService(被动接收注入)
public class OrderService
{
// 1. 声明依赖:通过构造函数告诉容器"我需要ILogService"
private readonly ILogService _logService;
public OrderService(ILogService logService)
{
_logService = logService; // 容器注入的实例
}
// 业务方法:使用依赖的服务
public void CreateOrder(int orderId)
{
_logService.Log($"创建订单:{orderId}"); // 用注入的日志服务
}
}
class Program
{
static void Main()
{
// 步骤1:注册服务(把服务映射关系添加到容器)
var services = new ServiceCollection();
// 注册:ILogService接口 → ConsoleLogService实现类(瞬时生命周期)
services.AddTransient<ILogService, ConsoleLogService>();
// 注册业务类OrderService
services.AddTransient<OrderService>();
// 步骤2:构建DI容器(IServiceProvider)
using var serviceProvider = services.BuildServiceProvider();
// 步骤3:从容器解析OrderService(容器自动注入ILogService)
var orderService = serviceProvider.GetRequiredService<OrderService>();
// 步骤4:调用业务方法(使用注入的日志服务)
orderService.CreateOrder(1001);
}
}
输出结果:
cs
【控制台日志】创建订单:1001
💡服务定位器
"服务定位器"是实现"控制反转"思想的另一种方式,
核心逻辑是:通过一个「全局可访问的 "定位器"(通常是.NET 的
IServiceProvider)」,让类主动从容器中获取所需的服务实例,而非被动接收注入("依赖注入" 是被动)。简单说:
- 依赖注入(DI)是 "容器把服务送上门"(被动接收);
- 服务定位器是 "类自己去容器里拿服务"(主动获取)。
优点是:灵活(可动态获取服务)
缺点:依赖关系 "隐藏"(代码中看不到类依赖哪些服务)
代码示例:
csusing System; using Microsoft.Extensions.DependencyInjection; // 业务类:主动从定位器(IServiceProvider)获取ILogService public class OrderService { // 1. 持有定位器(IServiceProvider)引用 private readonly IServiceProvider _serviceLocator; public OrderService(IServiceProvider serviceLocator) { _serviceLocator = serviceLocator; // 注入定位器(仅这一步是DI) } public void CreateOrder(int orderId) { // 2. 主动从定位器中获取ILogService(服务定位器核心逻辑) var logService = _serviceLocator.GetRequiredService<ILogService>(); logService.Log($"创建订单:{orderId}"); } } class Program { static void Main() { // 步骤1:注册服务(和DI示例完全一样) var services = new ServiceCollection(); services.AddTransient<ILogService, ConsoleLogService>(); services.AddTransient<OrderService>(); // 步骤2:构建容器(定位器本身) using var serviceProvider = services.BuildServiceProvider(); // 步骤3:解析OrderService(容器注入IServiceProvider) var orderService = serviceProvider.GetRequiredService<OrderService>(); // 步骤4:调用业务方法(主动获取日志服务) orderService.CreateOrder(1002); } }
相关概念
服务 :即对象(提供特定功能的对象)
在 DI 中通常以「接口 + 实现类」的形式存在(也可直接是具体类)
如上例中,ConsoleLogService 和ILogService 就是服务,具体的说前者是服务实现类,后者是服务接口。
注册服务:将服务映射关系告知容器的过程
核心定义:把「服务接口与实现类的对应关系」、「服务的生命周期」这两类信息,提交给服务容器的操作,相当于给容器 "下达指令"。
核心目的:让服务容器知道 "当需要某个服务接口的实例时,应该创建哪个实现类的对象,以及这个对象的生命周期如何"。
cs// 注册:ILogService(接口)→ ConsoleLogService(实现类),生命周期为瞬态 services.AddTransient<ILogService, ConsoleLogService>();
服务容器:负责管理注册的服务(DI 的 "核心管家")
核心定义:.NET 中对应IServiceProvider(构建后)/IServiceCollection(注册时),是存储所有注册服务信息、管理服务实例创建 / 销毁 / 依赖解析的核心容器。
如上例:运行阶段容器:IServiceProvider
注册阶段容器:ServiceCollection
核心管理职责(不止是 "存储",更核心是 "管理"):
存储:保存所有服务的注册信息(接口 - 实现类映射、生命周期);
创建:根据注册信息,在需要时创建服务实例;
解析:自动解析服务的依赖链(如 A 依赖 B,B 依赖 C,容器会自动创建 C→B→A);
销毁:根据服务生命周期,在合适时机销毁服务实例,释放资源。
查询服务:创建对象及关联对象(也叫 "服务解析")
核心定义:从服务容器中获取已注册服务实例的过程,容器在这个过程中会自动完成「当前服务对象创建」+「关联依赖对象创建与注入」,最终返回可用的服务实例。
核心细节: 不是简单的 "取对象",而是 "按需创建 + 依赖组装":如果服务 A 依赖服务 B,容器会先创建服务 B,再将 B 注入到 A 中,最终返回完整可用的 A 实例;
核心方法:.NET 中常用GetRequiredService<T>()(强制获取,无实例抛异常)、GetService<T>()(可选获取,无实例返回 null)。
cs// 这行代码就是「查询服务」的核心实现 //从IServiceProvider(运行阶段容器)中,查询并获取OrderService类型的服务实例; var orderService = serviceProvider.GetRequiredService<OrderService>();
对象生命周期:Transient (瞬态);Scoped(范围);Singleton(单例)
-
核心定义:服务实例的 "存活规则",决定了服务实例的「创建时机」和「销毁时机」,.NET DI 内置 3 种核心生命周期,适配不同场景需求。
-
三种生命周期核心拆解(通俗化):
生命周期类型 核心特点(创建 / 销毁) 通俗类比 适用场景 Transient(瞬态) 每次查询 / 注入都创建新实例,无主动销毁(由 GC 回收) 餐厅的一次性餐具,每次使用都换新的 无状态工具类(如日志、仓储、辅助计算) Scoped(范围) 同一 "作用域" 内(如ASP.NET Core 单个请求)创建1 个实例,作用域结束时销毁 餐厅的一桌餐具,同一桌顾客共用一套,用餐结束后回收 ASP.NET Core 请求相关服务(如用户上下文、请求仓储) Singleton(单例) 整个应用程序生命周期内只创建1 个实例,应用停止时销毁 餐厅的冰箱,所有厨师共用一个,餐厅关门后才停用 全局共享无状态服务(如配置、缓存、日志工厂)
根据类型获取 / 注册服务:
有两种类型:服务类型 vs 实现类型"
两者的关系:可以 "不同",也可以 "相同" ,推荐不同
如果一个服务是用接口+实现类 实现的,那么服务类型和实现类型分别是接口和实现类
如果一个服务就是 用一个类实现的,那么服务类型和实现类型就均是这个类。
比如之前的日志服务:
ILogService(接口,约定 "要做日志功能");
实现类型:ConsoleLogService(类,实际写日志到控制台)。
代码里的注册就是 "把招牌和干活的人绑定": 运行 // 服务类型(ILogService) → 实现类型(ConsoleLogService) services.AddTransient<ILogService, ConsoleLogService>();
cs// 服务类型(ILogService) → 实现类型(ConsoleLogService) services.AddTransient<ILogService, ConsoleLogService>();两者相同(直接注册类,不用接口):如果不写接口,直接注册一个类(比如OrderService): 服务类型 = 实现类型,服务类型和实现类型都是都是OrderService;
csOrderService services.AddTransient<OrderService>();
为什么 "建议面向接口编程(服务类型用接口)"?
用餐厅类比:如果招牌是 "川菜馆"(接口,只说 "做川菜"),实际干活的可以是张师傅、李师傅(不同实现类)------想换厨师,直接换干活的人,不用换门口的招牌(业务代码不用改)。
但如果招牌直接写 "张师傅川菜馆"(服务类型用具体类),想换李师傅就得把招牌也换成 "李师傅川菜馆"(业务代码必须改)。 所以用接口当服务类型,换实现更灵活,业务代码不用动(这就是 "面向接口编程" 的好处)。
".NET 的 DependencyInjection 组件,包含 ServiceLocator 的功能"
.NET 官方的这个 "DependencyInjection 组件"(就是我们一直用的ServiceCollection+IServiceProvider),不止能做 DI(被动注入),还能做服务定位器(主动拿服务)。
比如:
cs
// 这行就是"服务定位器"的功能(主动从容器拿服务)
var orderService = serviceProvider.GetRequiredService<OrderService>();
所以这个组件是 "一专多能"------ 名字叫 "依赖注入",但把 "服务定位器" 的功能也包进去了,既支持被动注入,也支持主动取服务。
初步使用
.NET 的 "依赖注入" 功能不是所有项目都默认自带的(比如控制台项目),这个命令是从官方 "软件商店"(NuGet)下载并安装 DI 所需的所有工具类(比如ServiceCollection、IServiceProvider),让你的项目能用上 DI。
cs
Install-Package Microsoft.Extensions.DependencyInjection
"using + 命名空间" 是告诉 C# 代码:"我要用到刚才安装的 DI 工具包里的类了",这样后续写ServiceCollection、IServiceProvider时,代码才认识这些类型(不会报错)。
cs
using Microsoft.Extensions.DependencyInjection
ServiceCollection和IServiceProvider的关系
这是把 "服务清单" 变成 "可用的容器"。
- 先理解
ServiceCollection:它是 DI 的 "服务清单"------ 你先把要管理的服务(比如之前的ILogService、OrderService)都 "登记" 到这个清单里(就像做饭前先列一个 "要准备的食材清单")。 - 再理解
BuildServiceProvider():把这个 "服务清单" 变成能实际用的 DI 容器(IServiceProvider)(类比:拿着食材清单去超市,把所有食材买回来,放到厨房储物柜里 ------ 这个储物柜就是 "容器",能直接拿里面的食材做饭)。 - 最终效果:这个容器(
IServiceProvider)可以取出 "清单里登记过的服务"(比如之前代码里用serviceProvider.GetRequiredService<OrderService>()拿服务)。
cs
// 步骤3:在"清单"里登记服务
services.AddTransient<ILogService, ConsoleLogService>();
// 步骤3:把"清单"变成可用的容器
using var serviceProvider = services.BuildServiceProvider();
// 步骤3:从容器里取服务(用之前登记的东西)
var orderService = serviceProvider.GetRequiredService<OrderService>();
初步实操
先定义一个服务:由接口和实现类组成:
cs
//服务接口
public interface ITestService
{
public string Name { get; set; }
public void SayHi();
}
//服务实现类
public class TestServiceImpl : ITestService
{
public string Name { get; set; }
public void SayHi()
{
Console.WriteLine($"Hi,Im{Name}");
}
}
普通方法调用这个服务:
cs
static void Main(string[] args)
{
ITestService p1 = new TestServiceImpl();
p1.Name = "Tom";
p1.SayHi();
Console.Read();
}

使用服务定位器:
cs
static void Main(string[] args)
{
//初始化服务注册集合
ServiceCollection services = new ServiceCollection();
//注册服务(写一个购物清单)
services.AddTransient<TestServiceImpl>();
// 构建正式DI容器(将清单物品买来放仓库里)
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
//解析服务(从容器取实例)
TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
p1.Name = "boy";
p1.SayHi();
}
}

生命周期-Singleton
cs
services.AddSingleton<TestServiceImpl>();
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
解析服务(从容器取实例)
TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
p1.Name = "boy";
p1.SayHi();
TestServiceImpl p2 = serviceProvider.GetService<TestServiceImpl>();
p2.Name = "jack";
p2.SayHi();
p1.SayHi();//如果他的结果为Jack,说明是同一对象
Console.WriteLine(object.ReferenceEquals(p1, p2));//Ture
object.ReferenceEquals这个方法用于判断2个实例是否为同一对象
}
如果是上个生命周期,则为false
三种生命周期-Scope
cs
services.AddScoped<TestServiceImpl>();
//Scope的写法有不同处,1、需要创建范围。2、解析的时候,不是直接用容器了,而是用scope中的容器
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
{
//TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
//但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
p3.Name = "any";
p3.SayHi();
}
}
创建了2个scope,验证里面的对象是否为同一对象,结果:不是
cs
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
TestServiceImpl t;
using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
{
//TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
//但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
p3.Name = "any";
p3.SayHi();
t = p3;
}
using (IServiceScope scope2 = serviceProvider.CreateScope())//初始化一个范围。
{
//TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
//但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
TestServiceImpl p3 = scope2.ServiceProvider.GetService<TestServiceImpl>();
p3.Name = "any";
p3.SayHi();
Console.WriteLine(object.ReferenceEquals(t, p3));
}
}
实现 IDisposable 的类,容器会自动调用 Dispose
通俗讲
IDisposable 是 "资源清洁工" 接口 ------ 如果你的类用了文件、数据库连接 这些 "用完必须手动关闭" 的资源,就实现这个接口,把 "关资源" 的代码写在Dispose方法里。
而.NET DI 容器是个 "贴心管家":如果这个类是Scoped/Transient生命周期,等它 "离开自己的使用范围"(比如 Scoped 的作用域结束、Transient 用完被回收),容器会自动帮你调用 Dispose 方法 ,不用你手动写using或者调用 Dispose 了。
cs
using Microsoft.Extensions.DependencyInjection;
using System;
namespace DILifeCycle
{
// 实现IDisposable的"文件服务"(用了文件资源,需要清理)
public class FileService : IDisposable
{
public FileService()
{
Console.WriteLine("FileService被创建了(打开了文件资源)");
}
// 资源清理逻辑写在这里
public void Dispose()
{
Console.WriteLine("FileService的Dispose被自动调用了(关闭了文件资源)");
}
}
class Program
{
static void Main()
{
var services = new ServiceCollection();
// 注册为Scoped(作用域内生效)
services.AddScoped<FileService>();
using var provider = services.BuildServiceProvider();
using (var scope = provider.CreateScope()) // 作用域开始
{
var fileService = scope.ServiceProvider.GetRequiredService<FileService>();
Console.WriteLine("正在使用FileService...");
} // 作用域结束 → 容器自动调用Dispose
Console.Read();
}
}
}
cs
FileService被创建了(打开了文件资源)
正在使用FileService...
FileService的Dispose被自动调用了(关闭了文件资源)
别在 "长生命周期对象" 里引用 "短生命周期对象"
通俗讲
生命周期有 "长短排名":Singleton(全局唯一,跟程序同生共死) > Scoped(比如一个请求活一个) > Transient(用一次就扔)
如果 "长寿的"(比如 Singleton)引用了 "短命的"(比如 Scoped),等 "短命的" 被销毁后,"长寿的" 还拿着它的引用 ------ 再用就会拿到 "已经死了的对象",ASP.NET Core 里会直接抛异常阻止这种危险操作。
3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core 中,这样做默认会抛异常。
4、生命周期的选择:如果类无状态,建议为 Singleton;如果类有状态,且有 Scope 控制,建议为 Scoped,因为通常这种 Scope 控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用 Transient 的时候要谨慎。
5、.NET 注册服务的重载方法很多,看着文档琢磨吧。
正式操作
