跳到主要内容

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 时,容器会:

  1. 发现 OrderService 的构造函数需要 IMessageSender
  2. 找到注册的 EmailSender 并创建它;
  3. EmailSender 送进 OrderService
  4. 把创建好的 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. 初学阶段先掌握这些

建议按这个顺序理解:

  1. 为什么不要自己 new 依赖
  2. 把依赖抽象成接口
  3. 通过构造函数接收依赖
  4. Program.cs 里注册服务
  5. 三种生命周期怎么选
  6. 在 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”,而是让代码依赖“接口”而不是“具体实现”,这样才能在需求变化时用“替换”代替“改代码”。