玩转 .NET 依赖注入:Microsoft.Extensions.DependencyInjection 深度指南

💡 前言

在现代 .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");
        }
    }
}

📌 测试小贴士 :切换注释中的 AddTransientAddScopedAddSingleton,运行程序观察控制台输出的 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 生态中绝对的核心。

  • 牢记 TransientScopedSingleton 的区别。

  • 编写代码时尽量依赖接口而非具体实现,满足 DIP(依赖倒置原则)。

  • 注意生命周期陷阱 ,善用 validateScopes

掌握了这些,不管是维护企业级老项目,还是架构全新的微服务,你都能在依赖管理上游刃有余。

相关推荐
随心(cracker)3 小时前
BeaverX Admin:一套真正能跑起来的开源管理后台
vue·.net·管理后台
忧郁的紫菜3 小时前
基础实现:单篇 Markdown 转 Word
开发语言·c#·word
甜美的小天鹅3 小时前
Swifter C#之inline还是不inline,这是个问题
开发语言·c#
忧郁的紫菜7 小时前
异步编程 In .NET
.net
翼帆7 小时前
.NET 程序保护实战系列01-流水线架构与保护引擎总览
c#·破解
落寞的电源9 小时前
Delegate = Object + MethodInfo
开发语言·数据库·c#
Ricky_Theseus11 小时前
Trie 字典树:前缀匹配利器
开发语言·c#
北域码匠1 天前
嵌入式限幅滤波:工业信号降噪利器
c#·传感器采集·数据预处理·嵌入式算法·限幅滤波·数字滤波·数据降噪
长孙豪翔1 天前
在.net中读写config文件的各种方法
java·数据库·.net