文档说明:本文档旨在快速梳理 C# 中依赖注入(Dependency Injection, DI)的常见注入方式、生命周期区别、单/多实例的注册与接收语法,以及多重注入时的调用策略,便于日常开发查阅。
一、 核心问题:依赖注入必须写接口吗?
结论:不是必须的。完全可以直接注入具体的类。
1. 方式 A:直接注入具体类 (Concrete Class)
不需要定义接口,直接将类本身注册到 DI 容器中。
-
注册语法 :
builder.Services.AddTransient<TagProcessJob>(); -
接收语法 :构造函数中直接接收
TagProcessJob。 -
适用场景:
- 后台任务 / 定时作业(如 Hangfire/Quartz 的 Job 类)。
- 无状态的简单工具类(纯数学计算、日期格式化等)。
- 绝对不需要在单元测试中被 Mock(伪造)的类。
2. 方式 B:注入接口与实现类 (Interface & Implementation)
定义一个接口,并告诉 DI 容器该接口对应的具体实现类。
-
注册语法 :
builder.Services.AddScoped<ITagService, TagService>(); -
接收语法 :构造函数中接收接口
ITagService。 -
适用场景(推荐绝大多数业务使用) :
- 核心业务逻辑 (如
UserService,OrderService)。 - 需要连接数据库或外部 API 的服务。
- 需要编写单元测试(通过接口可以轻松使用 Moq 等框架伪造假数据,不连接真实数据库)。
- 未来可能替换实现(如将微信支付替换为支付宝,只需修改一行注册代码,业务代码全都不用改)。
- 核心业务逻辑 (如
二、 DI 的三种生命周期 (Lifetimes)
理解生命周期的核心在于:DI 容器(对象工厂)什么时候给你创建新对象,什么时候给你复用旧对象。
| 生命周期 | 行为特点 | 适用场景 | 注册语法示例 (以具体类为例) |
|---|---|---|---|
| Transient (瞬态) | 每次请求,都是全新 。 不管在何处获取,每次都会 new 一个新实例。 |
轻量级、无状态的服务。 如简单的计算类、后台 Job。 | builder.Services.AddTransient<MyJob>(); |
| Scoped (作用域) | 同一次 HTTP 请求内唯一。 一个请求的上下文中共享同一个实例;不同请求之间实例不同。 | 最常用的业务周期 。 如数据库连接 (DbContext),保证同一次请求内的事务一致性。 |
builder.Services.AddScoped<MyJob>(); |
| Singleton (单例) | 全局唯一,从一而终。 程序启动创建后,整个应用生命周期内共享这一个实例。 | 需要全局共享状态的服务。 如内存缓存、配置读取器、RabbitMQ 长连接。(需注意线程安全) | builder.Services.AddSingleton<MyJob>(); |
三、 "单个"与"多个"注入的语法对比
(注:注入"多个"通常发生在使用接口的场景下,即一个接口有多种实现)
1. 单个注入 (最常见)
无论是具体类还是接口,直接注册即可。
C#
// 注册 (以接口为例)
builder.Services.AddScoped<IPaymentService, WeChatPayService>();
// 接收 (在 Controller 或 Service 中)
public class OrderController
{
private readonly IPaymentService _payment;
public OrderController(IPaymentService payment) => _payment = payment;
}
2. 多个注入 - 方式一:标准多注入 (IEnumerable)
C#
// 1. 注册 (多次注册同一个接口的不同实现)
builder.Services.AddTransient<IMessageSender, EmailSender>();
builder.Services.AddTransient<IMessageSender, SmsSender>();
// 2. 接收 (必须使用 IEnumerable<T> 包裹)
public class NotificationController
{
private readonly IEnumerable<IMessageSender> _senders;
public NotificationController(IEnumerable<IMessageSender> senders)
{
_senders = senders;
}
}
3. 多个注入 - 方式二:键控注入 (Keyed Services, .NET 8+)
C#
// 1. 注册 (使用 AddKeyed... 方法,并传入字符串标识)
builder.Services.AddKeyedScoped<IPaymentService, WeChatPayService>("WeChat");
builder.Services.AddKeyedScoped<IPaymentService, AliPayService>("AliPay");
// 2. 接收 (使用 [FromKeyedServices] 特性精准获取)
public class CheckoutController
{
private readonly IPaymentService _payment;
public CheckoutController([FromKeyedServices("WeChat")] IPaymentService payment)
{
_payment = payment;
}
}
四、 进阶案例:多个注入时,如何区分并调用同名方法?
既然多个实现类继承了同一个接口,它们的方法名(例如 .Pay())必然是完全相同的。区分的关键不在于"调用的方法名",而在于你"如何从集合中拿到正确的那个对象实例"。
场景 1:你想让它们"全部执行一次"(广播模式)
适用情况:群发通知、执行所有数据验证规则。
调用方式 :通过 IEnumerable<T> 注入后,直接使用 foreach 循环调用。
C#
public class NotificationController
{
private readonly IEnumerable<IMessageSender> _senders;
public NotificationController(IEnumerable<IMessageSender> senders)
{
_senders = senders; // 里面同时包含了 EmailSender 和 SmsSender
}
public void SendAll(string message)
{
// 循环调用同名方法 .Send(),大家一起执行
foreach (var sender in _senders)
{
sender.Send(message);
}
}
}
场景 2:你想"根据条件挑出一个"来执行(策略模式 - .NET 8 之前的老写法)
适用情况:根据用户的选择(如微信/支付宝),只执行其中一个的具体逻辑。
调用方式:必须在接口中增加一个"标识属性",然后在调用时通过 LINQ 筛选。
C#
// 1. 接口中增加标识属性
public interface IPaymentService
{
string PayType { get; } // 用于自报家门
void Pay();
}
// 2. 实现类各自标明身份
public class WeChatPayService : IPaymentService
{
public string PayType => "WeChat";
public void Pay() { /* 微信逻辑 */ }
}
public class AliPayService : IPaymentService
{
public string PayType => "AliPay";
public void Pay() { /* 支付宝逻辑 */ }
}
// 3. 业务代码中进行筛选和调用
public class CheckoutController
{
private readonly IEnumerable<IPaymentService> _payments;
public CheckoutController(IEnumerable<IPaymentService> payments)
{
_payments = payments;
}
public void ProcessOrder(string userSelectedPayType) // 假设传入了 "WeChat"
{
// 核心:从集合中找出身分为 "WeChat" 的那个服务实例
var targetService = _payments.FirstOrDefault(p => p.PayType == userSelectedPayType);
if (targetService != null)
{
// 此时 targetService 是 WeChatPayService 的实例,执行它的 Pay()
targetService.Pay();
}
}
}
场景 3:精确点名调用(策略模式 - .NET 8 键控服务推荐写法)
适用情况:在确定需要特定实现的场景中,代码最简洁,无需遍历集合。
调用方式 :利用 [FromKeyedServices("别名")] 直接拿到对应的实例。
C#
public class CheckoutController
{
private readonly IPaymentService _wechatPayment;
private readonly IPaymentService _aliPayment;
// 在构造函数里,明确告诉 DI 容器我们要谁
public CheckoutController(
[FromKeyedServices("WeChat")] IPaymentService wechatPayment,
[FromKeyedServices("AliPay")] IPaymentService aliPayment)
{
_wechatPayment = wechatPayment; // 装的是微信实例
_aliPayment = aliPayment; // 装的是支付宝实例
}
public void PayWithWeChat()
{
// 直接调用,执行的一定是微信的逻辑
_wechatPayment.Pay();
}
}