ASP.NET Core 依赖注入
依赖注入(Dependency Injection,简称 DI)就是:一个类需要用到别的东西时,不自己“new”,而是让外部把它“送进来”。
1. 没有 DI 的问题
假设 OrderService 需要发通知,它自己创建了一个 EmailSender:
public class OrderService
{
private readonly EmailSender _sender = new EmailSender();
public void CreateOrder()
{
// 业务逻辑...
_sender.Send("订单已创建");
}
}
看起来能用,但问题很快出现:
OrderService被“焊死”在EmailSender上,想换成短信就得改代码。- 写单元测试时,一调用就真的发邮件,无法替换成假的实现。
EmailSender如果自己也依赖别的东西,创建过程会越来越复杂。
根源在于:OrderService 自己决定了用哪个具体实现。
2. 用 DI 改造
第一步,把依赖抽象成接口:
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"发送邮件:{message}");
}
}
第二步,OrderService 不再自己 new,而是通过构造函数“接收”依赖:
public class OrderService
{
private readonly IMessageSender _sender;
public OrderService(IMessageSender sender)
{
_sender = sender;
}
public void CreateOrder()
{
// 业务逻辑...
_sender.Send("订单已创建");
}
}
现在 OrderService 只关心“有一个能发消息的东西”,而不关心它到底是邮件还是短信。具体用哪个,交给外部决定。
3. 容器负责存储依赖
在 ASP.NET Core 里,负责创建对象、并把依赖送进去的角色叫 容器(IoC Container),也叫 服务提供者(Service Provider)。
你只需要在 Program.cs 里告诉容器:“遇到 IMessageSender,就给我一个 EmailSender”。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMessageSender, EmailSender>();
builder.Services.AddScoped<OrderService>();
var app = builder.Build();
之后当某个地方需要 OrderService 时,容器会:
- 发现
OrderService的构造函数需要IMessageSender; - 找到注册的
EmailSender并创建它; - 把
EmailSender送进OrderService; - 把创建好的
OrderService交给你。
这一整套“自动帮你组装对象”的过程,就是依赖注入的核心价值。
4. DI 解决的问题
初学阶段,重点记住下面这些作用,尤其是**“可替换”**这一条。
4.1 依赖可以随时替换(最重要)
因为 OrderService 依赖的是接口 IMessageSender,你可以在不改动 OrderService 一行代码的情况下换掉具体实现:
// 默认:所有业务都用邮件通知
builder.Services.AddScoped<IMessageSender, EmailSender>();
// 换成短信,只改这一行注册即可
builder.Services.AddScoped<IMessageSender, SmsSender>();
4.2 特殊需求替换通用逻辑
真实项目里经常出现这种情况:大部分场景用通用实现,某个特殊需求要用不一样的逻辑。
比如平时用真实的邮件发送,但某个大客户要求“通知必须走他们自建的内部网关”,这时只需要写一个新的实现类,然后替换注册:
// 通用实现
public class EmailSender : IMessageSender { /* ... */ }
// 针对特定客户的特殊实现
public class CustomerGatewaySender : IMessageSender
{
public void Send(string message)
{
// 调用该客户专用网关
}
}
// 通用环境
builder.Services.AddScoped<IMessageSender, EmailSender>();
// 该客户的部署环境:只替换注册,业务代码完全不动
builder.Services.AddScoped<IMessageSender, CustomerGatewaySender>();
OrderService 完全感知不到这次替换——它只知道“我调用了 Send”。这就是 DI 最有价值的地方:用“替换实现”代替“修改逻辑”。
4.3 方便写测试
测试时可以送进一个假的实现,不真正发消息:
public class FakeSender : IMessageSender
{
public string? LastMessage { get; private set; }
public void Send(string message) => LastMessage = message;
}
var fake = new FakeSender();
var service = new OrderService(fake);
service.CreateOrder();
// 断言业务确实触发了通知
Assert.Equal("订单已创建", fake.LastMessage);
4.4 统一管理对象的创建和生命周期
对象由容器统一创建,你不用关心“先建谁、后建谁”,也不用手动释放资源,容器会按规则处理。
5. 三种生命周期
注册服务时要选择它“活多久”,这叫 生命周期(Lifetime):
| 方法 | 生命周期 | 每次都是新对象吗 | 常见用途 |
|---|---|---|---|
AddTransient | 瞬时 | 每次获取都新建 | 轻量、无状态的小工具 |
AddScoped | 作用域 | 同一次请求内共用一个 | 大多数业务 Service |
AddSingleton | 单例 | 整个应用只有一个 | 全局配置、缓存 |
一句话记忆:
- Transient:用完就扔,每次都新的。
- Scoped:一次 HTTP 请求内是同一个。
- Singleton:应用启动到关闭,始终是同一个。
Web API 里最常用的是 Scoped。参考 微软依赖注入官方文档。
6. 在 Controller 中使用
Controller 也是由容器创建的,所以直接在构造函数里“要”依赖即可:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
public OrdersController(OrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public IActionResult Create()
{
_orderService.CreateOrder();
return Ok();
}
}
你不需要在任何地方 new OrderService(...),容器会自动把它连同它的依赖一起组装好送进来。
7. 三种注入方式
最常用、也最推荐的是 构造函数注入(前面的例子都是)。另外还有两种,了解即可:
// 方式一:构造函数注入(推荐)
public OrdersController(OrderService orderService) { }
// 方式二:Action 方法参数注入,用 [FromServices]
[HttpGet]
public IActionResult Get([FromServices] OrderService orderService)
{
return Ok();
}
初学阶段优先使用构造函数注入,依赖关系一眼就能看清。
8. 初学阶段先掌握这些
建议按这个顺序理解:
- 为什么不要自己
new依赖 - 把依赖抽象成接口
- 通过构造函数接收依赖
- 在
Program.cs里注册服务 - 三种生命周期怎么选
- 在 Controller 中注入使用
核心结构可以先记住:
// 1. 定义接口
public interface IMessageSender { void Send(string message); }
// 2. 实现接口
public class EmailSender : IMessageSender { /* ... */ }
// 3. 注册(换实现只改这里)
builder.Services.AddScoped<IMessageSender, EmailSender>();
// 4. 构造函数注入使用
public OrderService(IMessageSender sender) { }
初学时尤其记住:依赖注入的关键不是“少写一个 new”,而是让代码依赖“接口”而不是“具体实现”,这样才能在需求变化时用“替换”代替“改代码”。