深入理解 .NET 中的依赖注入(DI):从原理到实战
依赖注入(Dependency Injection,简称 DI)是现代 .NET 开发中不可或缺的一部分。无论是 ASP.NET Core Web API、MVC、Worker Service 还是 Blazor 项目,都离不开依赖注入。DI 提供了一种松耦合、可测试并易于维护的代码结构,也让整个应用的架构更加清晰。
本篇文章将从 DI 的核心概念、常见的注册方式到实际示例,帮助你真正理解 DI 的价值。
一、什么是依赖注入?
依赖注入是一种实现 控制反转(IoC) 的方式。
传统方式中,一个类如果需要一个服务,会自己在内部创建:
csharp
public class OrderService
{
private readonly EmailSender _emailSender = new EmailSender();
public void CreateOrder()
{
_emailSender.Send("订单创建成功");
}
}
这种写法的缺点:
- 强耦合:OrderService 依赖 EmailSender 的实现
- 难以测试:单元测试无法替换 EmailSender
- 难以扩展:如果邮件发送方式改变,需要修改 OrderService 代码
而通过 DI,将依赖关系"注入"进来:
csharp
public class OrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public void CreateOrder()
{
_emailSender.Send("订单创建成功");
}
}
这样 OrdersService 完全不关心 EmailSender 的具体实现,更加灵活、可扩展。
二、.NET 默认提供的三种服务生命周期
在 .NET 中注册依赖时,需要指定对象的生命周期,框架内置了三种方式:
1. Transient(瞬时)
每次请求都会创建一个新的实例
ini
services.AddTransient<IEmailSender, EmailSender>();
适合:
- 无状态对象
- 轻量级、频繁使用的服务
2. Scoped(作用域)
在一个请求作用域内共享同一个实例,主要用于 Web 应用。
ini
services.AddScoped<IOrderRepository, OrderRepository>();
适合:
- 与数据库交互的服务
- 需要在一次请求中共享数据的服务
3. Singleton(单例)
应用程序整个生命周期内只有一个实例。
ini
services.AddSingleton<ICacheService, MemoryCacheService>();
适合:
- 缓存类服务
- 配置类服务
⚠️ 注意:Singleton 不能依赖 Scoped 服务,否则会产生 "依赖链生命周期问题"。
三、构造函数注入:最推荐的 DI 方式
构造函数注入是最常见、最推荐的注入方式:
csharp
public class OrderService : IOrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public void CreateOrder()
{
_emailSender.Send("您的订单已创建");
}
}
优点:
- 清晰地表达依赖关系
- 便于单元测试
- 强制依赖不可为空(可结合 C# 的
readonly/nullable)
四、方法注入 & 属性注入
方法注入
依赖只用于方法内部时可以这样:
arduino
public void SendOrder(IEmailSender emailSender)
{
emailSender.Send("订单通知");
}
属性注入(不推荐)
arduino
public IEmailSender EmailSender { get; set; }
缺点:
- 依赖是否注入不明确
- 可能导致空引用异常
五、实际示例:在 ASP.NET Core 中使用依赖注入
假设我们有一个订单服务:
1. 定义接口与实现
csharp
public interface IOrderService
{
void Create(string name);
}
public class OrderService : IOrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void Create(string name)
{
_logger.LogInformation($"订单 {name} 已创建");
}
}
2. 注册服务
ini
builder.Services.AddScoped<IOrderService, OrderService>();
3. 注入并使用
csharp
[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public IActionResult Create(string name)
{
_orderService.Create(name);
return Ok("订单创建成功");
}
}
六、为什么需要依赖注入?
总结一下 DI 的核心价值:
- 降低耦合:实现与使用分离
- 提高可测试性:方便 Mock 依赖
- 更易扩展:支持多实现切换
- 提升代码结构清晰度
你会发现在大型项目中,DI 会让代码组织更加清晰,模块之间关系更明确。
结语
依赖注入是 .NET 中最重要的应用程序结构基础之一。掌握 DI 你就掌握了现代 .NET 架构的核心能力,无论是 Web API、微服务还是桌面应用,都离不开 DI 的支撑。