💡 前言
在现代 .NET 开发(自 .NET Core 起)中,依赖注入(Dependency Injection,简称 DI) 已经成为了框架的基石。微软官方提供了 Microsoft.Extensions.DependencyInjection 库,它是一个轻量级、高性能的控制反转(IoC)容器。
无论是开发 ASP.NET Core Web API、Worker Service 还是传统的 Console 控制台程序,掌握这个库的用法都是每个 .NET 开发者迈向中高级的必经之路。本文将带你从零开始,彻底掌握它的核心用法与最佳实践。
🛠️ 一、 核心概念与三个生命周期
在学习如何使用之前,必须先理解依赖注入的三种服务生命周期。理解它们能帮你避免绝大多数因生命周期错乱导致的 Bug(比如在单例服务中注入瞬时服务)。
| 生命周期 | 注册方法 | 表现行为 | 适用场景 |
|---|---|---|---|
| Transient(瞬时) | AddTransient |
每次请求都会创建一个新的实例。 | 轻量级、无状态的服务,如工具类。 |
| Scoped(作用域) | AddScoped |
在**同一个作用域(Scope)**内只创建一次。在 Web 开发中,通常对应一次 HTTP 请求。 | 带有状态的业务逻辑层(Service)、数据库上下文(DbContext)。 |
| Singleton(单例) | AddSingleton |
整个应用程序运行期间只创建一次,后续所有请求共用这一个实例。 | 配置管理类、缓存管理类、公共线程池等。 |
🚀 二、 基础上手:在控制台程序中使用
要在非 ASP.NET Core 环境(如控制台程序)中使用它,我们需要手动安装 Nuget 包:
Shell
dotnet add package Microsoft.Extensions.DependencyInjection
1. 完整代码示例
以下代码展示了如何初始化容器、注册不同生命周期的服务并获取它们:
C#
using System;
using Microsoft.Extensions.DependencyInjection;
namespace DiDemo
{
// 1. 定义接口与实现类
public interface IMessageService
{
void Send(string message);
}
public class EmailService : IMessageService
{
private readonly Guid _id = Guid.NewGuid();
public void Send(string message) => Console.WriteLine($"[Email-{_id}] 发送: {message}");
}
class Program
{
static void Main(string[] args)
{
// 2. 构建服务容器集合 (ServiceCollection)
var services = new ServiceCollection();
// 3. 注册服务
services.AddTransient<IMessageService, EmailService>(); // 瞬时
// services.AddScoped<IMessageService, EmailService>(); // 作用域
// services.AddSingleton<IMessageService, EmailService>(); // 单例
// 4. 构建服务提供程序 (ServiceProvider)
var serviceProvider = services.BuildServiceProvider();
// 5. 从容器中解析并使用服务
var msgService1 = serviceProvider.GetRequiredService<IMessageService>();
msgService1.Send("Hello World 1");
var msgService2 = serviceProvider.GetRequiredService<IMessageService>();
msgService2.Send("Hello World 2");
}
}
}
📌 测试小贴士 :切换注释中的
AddTransient、AddScoped和AddSingleton,运行程序观察控制台输出的Guid是否相同,就能直观感受到三种生命周期的区别!
🎯 三、 高级进阶用法
1. 构造函数注入(Constructor Injection)
DI 容器会自动寻找实现类的构造函数,并自动注入它所依赖的其他服务。
C#
public interface IOrderService { void CreateOrder(); }
public interface INotificationService { void Notify(); }
public class OrderService : IOrderService
{
private readonly INotificationService _notificationService;
// 容器会自动解析并传入 INotificationService 的实现
public OrderService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public void CreateOrder()
{
Console.WriteLine("订单创建成功!");
_notificationService.Notify();
}
}
2. 多个实现的注册与解析
如果一个接口有多个实现类,你可以全部注册它们:
C#
services.AddTransient<INotificationService, EmailNotification>();
services.AddTransient<INotificationService, SmsNotification>();
-
获取最后一个注册的实现 :使用
GetRequiredService<INotificationService>(),容器会返回最后注册的SmsNotification。 -
获取所有实现 :使用
GetServices<INotificationService>(),它会返回一个IEnumerable<INotificationService>集合,包含所有已注册的实现。
3. 使用 Scope(作用域)
在控制台或后台任务中,如果你需要手动隔离作用域(模拟 Web 的一次请求),可以使用 CreateScope():
C#
using (var scope = serviceProvider.CreateScope())
{
// 在这个 using 块内,所有 AddScoped 注册的服务只会实例化一次
var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
}
// 出了 using 块,该 scope 内创建的所有服务(实现了 IDisposable)都会自动释放
4. 尝试注册与替换
为了防止重复注册,或者在编写中间件、组件库时提供默认实现,可以使用 Microsoft.Extensions.DependencyInjection.Extensions 命名空间下的扩展方法:
C#
using Microsoft.Extensions.DependencyInjection.Extensions;
// TryAddTransient:只有当 IMessageService 没有被注册过时,才会注册 EmailService
services.TryAddTransient<IMessageService, EmailService>();
// TryAddEnumerable:防止注册完全相同的实现
services.TryAddEnumerable(ServiceDescriptor.Transient<IMessageService, EmailService>());
// Replace:替换已有的注册
services.Replace(ServiceDescriptor.Singleton<IMessageService, NewCoolService>());
⚠️ 四、 常见踩坑点与避坑指南
1. 什么是 Captive Dependency(捕获依赖)?
单例服务不能依赖生命周期比它短的服务!
例如:一个 Singleton 服务在构造函数中注入了一个 Scoped 服务。因为 Singleton 只实例化一次,导致那个 Scoped 服务也变成了单例,这会引发并发冲突或数据错乱(特别是注入 DbContext 时)。
💡 解决方案 :在构建
ServiceProvider时开启验证:C#
var serviceProvider = services.BuildServiceProvider(validateScopes: true);在 .NET 6/7/8+ 的 ASP.NET Core 默认模板中,开发环境(Development)已默认开启此项验证。
2. GetService vs GetRequiredService
-
GetService<T>():如果容器中找不到该服务,返回null。 -
GetRequiredService<T>():如果找不到服务,直接抛出InvalidOperationException异常。 -
最佳实践 :除非该服务是可选的(Optional),否则一律推荐使用
GetRequiredService<T>(),这样能尽早暴露配置错误。
📝 五、 总结
Microsoft.Extensions.DependencyInjection 以其轻量、高效和开箱即用的特点,成为了 .NET 生态中绝对的核心。
-
牢记 Transient 、Scoped 、Singleton 的区别。
-
编写代码时尽量依赖接口而非具体实现,满足 DIP(依赖倒置原则)。
-
注意生命周期陷阱 ,善用
validateScopes。
掌握了这些,不管是维护企业级老项目,还是架构全新的微服务,你都能在依赖管理上游刃有余。