C# / ASP.NET Core 依赖注入 (DI) 核心知识点

文档说明:本文档旨在快速梳理 C# 中依赖注入(Dependency Injection, DI)的常见注入方式、生命周期区别、单/多实例的注册与接收语法,以及多重注入时的调用策略,便于日常开发查阅。

一、 核心问题:依赖注入必须写接口吗?

结论:不是必须的。完全可以直接注入具体的类。

1. 方式 A:直接注入具体类 (Concrete Class)

不需要定义接口,直接将类本身注册到 DI 容器中。

  • 注册语法builder.Services.AddTransient<TagProcessJob>();

  • 接收语法 :构造函数中直接接收 TagProcessJob

  • 适用场景

    • 后台任务 / 定时作业(如 Hangfire/Quartz 的 Job 类)。
    • 无状态的简单工具类(纯数学计算、日期格式化等)。
    • 绝对不需要在单元测试中被 Mock(伪造)的类

2. 方式 B:注入接口与实现类 (Interface & Implementation)

定义一个接口,并告诉 DI 容器该接口对应的具体实现类。

  • 注册语法builder.Services.AddScoped<ITagService, TagService>();

  • 接收语法 :构造函数中接收接口 ITagService

  • 适用场景(推荐绝大多数业务使用)

    • 核心业务逻辑 (如 UserService, OrderService)。
    • 需要连接数据库或外部 API 的服务
    • 需要编写单元测试(通过接口可以轻松使用 Moq 等框架伪造假数据,不连接真实数据库)。
    • 未来可能替换实现(如将微信支付替换为支付宝,只需修改一行注册代码,业务代码全都不用改)。

二、 DI 的三种生命周期 (Lifetimes)

理解生命周期的核心在于:DI 容器(对象工厂)什么时候给你创建新对象,什么时候给你复用旧对象。

生命周期 行为特点 适用场景 注册语法示例 (以具体类为例)
Transient (瞬态) 每次请求,都是全新 。 不管在何处获取,每次都会 new 一个新实例。 轻量级、无状态的服务。 如简单的计算类、后台 Job。 builder.Services.AddTransient<MyJob>();
Scoped (作用域) 同一次 HTTP 请求内唯一。 一个请求的上下文中共享同一个实例;不同请求之间实例不同。 最常用的业务周期 。 如数据库连接 (DbContext),保证同一次请求内的事务一致性。 builder.Services.AddScoped<MyJob>();
Singleton (单例) 全局唯一,从一而终。 程序启动创建后,整个应用生命周期内共享这一个实例。 需要全局共享状态的服务。 如内存缓存、配置读取器、RabbitMQ 长连接。(需注意线程安全 builder.Services.AddSingleton<MyJob>();

三、 "单个"与"多个"注入的语法对比

(注:注入"多个"通常发生在使用接口的场景下,即一个接口有多种实现)

1. 单个注入 (最常见)

无论是具体类还是接口,直接注册即可。

C# 复制代码
// 注册 (以接口为例)
builder.Services.AddScoped<IPaymentService, WeChatPayService>();

// 接收 (在 Controller 或 Service 中)
public class OrderController
{
    private readonly IPaymentService _payment;
    public OrderController(IPaymentService payment) => _payment = payment;
}

2. 多个注入 - 方式一:标准多注入 (IEnumerable)

C# 复制代码
// 1. 注册 (多次注册同一个接口的不同实现)
builder.Services.AddTransient<IMessageSender, EmailSender>();
builder.Services.AddTransient<IMessageSender, SmsSender>();

// 2. 接收 (必须使用 IEnumerable<T> 包裹)
public class NotificationController
{
    private readonly IEnumerable<IMessageSender> _senders;
    public NotificationController(IEnumerable<IMessageSender> senders) 
    {
        _senders = senders;
    }
}

3. 多个注入 - 方式二:键控注入 (Keyed Services, .NET 8+)

C# 复制代码
// 1. 注册 (使用 AddKeyed... 方法,并传入字符串标识)
builder.Services.AddKeyedScoped<IPaymentService, WeChatPayService>("WeChat");
builder.Services.AddKeyedScoped<IPaymentService, AliPayService>("AliPay");

// 2. 接收 (使用 [FromKeyedServices] 特性精准获取)
public class CheckoutController
{
    private readonly IPaymentService _payment;
    public CheckoutController([FromKeyedServices("WeChat")] IPaymentService payment)
    {
        _payment = payment;
    }
}

四、 进阶案例:多个注入时,如何区分并调用同名方法?

既然多个实现类继承了同一个接口,它们的方法名(例如 .Pay())必然是完全相同的。区分的关键不在于"调用的方法名",而在于你"如何从集合中拿到正确的那个对象实例"。

场景 1:你想让它们"全部执行一次"(广播模式)

适用情况:群发通知、执行所有数据验证规则。

调用方式 :通过 IEnumerable<T> 注入后,直接使用 foreach 循环调用。

C# 复制代码
public class NotificationController
{
    private readonly IEnumerable<IMessageSender> _senders;

    public NotificationController(IEnumerable<IMessageSender> senders)
    {
        _senders = senders; // 里面同时包含了 EmailSender 和 SmsSender
    }

    public void SendAll(string message)
    {
        // 循环调用同名方法 .Send(),大家一起执行
        foreach (var sender in _senders)
        {
            sender.Send(message); 
        }
    }
}

场景 2:你想"根据条件挑出一个"来执行(策略模式 - .NET 8 之前的老写法)

适用情况:根据用户的选择(如微信/支付宝),只执行其中一个的具体逻辑。

调用方式:必须在接口中增加一个"标识属性",然后在调用时通过 LINQ 筛选。

C# 复制代码
// 1. 接口中增加标识属性
public interface IPaymentService
{
    string PayType { get; } // 用于自报家门
    void Pay();
}

// 2. 实现类各自标明身份
public class WeChatPayService : IPaymentService
{
    public string PayType => "WeChat"; 
    public void Pay() { /* 微信逻辑 */ }
}

public class AliPayService : IPaymentService
{
    public string PayType => "AliPay"; 
    public void Pay() { /* 支付宝逻辑 */ }
}

// 3. 业务代码中进行筛选和调用
public class CheckoutController
{
    private readonly IEnumerable<IPaymentService> _payments;

    public CheckoutController(IEnumerable<IPaymentService> payments)
    {
        _payments = payments;
    }

    public void ProcessOrder(string userSelectedPayType) // 假设传入了 "WeChat"
    {
        // 核心:从集合中找出身分为 "WeChat" 的那个服务实例
        var targetService = _payments.FirstOrDefault(p => p.PayType == userSelectedPayType);

        if (targetService != null)
        {
            // 此时 targetService 是 WeChatPayService 的实例,执行它的 Pay()
            targetService.Pay(); 
        }
    }
}

场景 3:精确点名调用(策略模式 - .NET 8 键控服务推荐写法)

适用情况:在确定需要特定实现的场景中,代码最简洁,无需遍历集合。

调用方式 :利用 [FromKeyedServices("别名")] 直接拿到对应的实例。

C# 复制代码
public class CheckoutController
{
    private readonly IPaymentService _wechatPayment;
    private readonly IPaymentService _aliPayment;

    // 在构造函数里,明确告诉 DI 容器我们要谁
    public CheckoutController(
        [FromKeyedServices("WeChat")] IPaymentService wechatPayment,
        [FromKeyedServices("AliPay")] IPaymentService aliPayment)
    {
        _wechatPayment = wechatPayment; // 装的是微信实例
        _aliPayment = aliPayment;       // 装的是支付宝实例
    }

    public void PayWithWeChat()
    {
        // 直接调用,执行的一定是微信的逻辑
        _wechatPayment.Pay(); 
    }
}
相关推荐
yuhaiqiang20 小时前
为什么我建议你不要只问一个AI?🤫偷偷学会“群发”,答案准到离谱!
人工智能·后端·ai编程
双向3321 小时前
AR 眼镜拯救社恐:我用 Kotlin 写了个拜年提词器
后端
吾日三省Java21 小时前
Spring Cloud架构下的日志追踪:传统MDC vs 王炸SkyWalking
java·后端·架构
想打游戏的程序猿21 小时前
服务端用AI写前端:隐患、困境与思考
后端
前端拿破轮21 小时前
从0到1搭建个人网站(三):用 Cloudflare R2 + PicGo 搭建高速图床
前端·后端·面试
树獭叔叔21 小时前
深度拆解 DiT:扩散模型与 Transformer 的巅峰结合
后端·aigc·openai
ZhengEnCi21 小时前
08c. 检索算法与策略-混合检索
后端·python·算法
用户7344028193421 天前
Java 8 Stream 的终极技巧——Collectors 操作
后端
树獭叔叔1 天前
深度拆解 VAE:生成式 AI 的潜空间大门
后端·aigc·openai