0

0

C#的依赖注入(Dependency Injection)如何实现?

星降

星降

发布时间:2025-08-06 12:14:02

|

696人浏览过

|

来源于php中文网

原创

c#中实现依赖注入的核心是通过ioc容器将对象创建与依赖解析从业务逻辑中解耦,推荐使用构造函数注入;2. 实现步骤包括定义服务接口、实现接口、在消费者类中通过构造函数接收依赖、使用servicecollection注册服务并构建服务提供者;3. 依赖注入的优势在于解耦、提升可测试性、可维护性和可扩展性;4. 常见注入方式有构造函数注入(最推荐)、属性注入(适用于可选依赖)和方法注入(适用于特定场景);5. 在asp.net core中,di由内置容器支持,服务在program.cs中通过addtransient、addscoped、addsingleton注册,容器在运行时自动解析构造函数中的依赖,实现无缝注入。

C#的依赖注入(Dependency Injection)如何实现?

C#中实现依赖注入,核心在于将对象的创建和依赖关系的解析从业务逻辑中解耦出来,通常会借助一个IoC(Inversion of Control)容器来管理这些对象的生命周期和依赖注入过程。最常见且推荐的做法是构造函数注入

解决方案

在C#中实现依赖注入,最直接且广泛采用的方式是结合接口和依赖注入容器。以下是一个基础的实现流程:

首先,你需要定义一个服务接口及其具体实现。这是DI的基础,因为我们总是面向接口编程。

// 1. 定义服务接口
public interface IMessageSender
{
    void SendMessage(string message);
}

// 2. 实现服务接口
public class EmailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Sending email: {message}");
        // 实际中这里会有更复杂的邮件发送逻辑
    }
}

// 3. 定义一个需要依赖的服务(消费者)
public class NotificationService
{
    private readonly IMessageSender _messageSender;

    // 构造函数注入:通过构造函数接收依赖
    public NotificationService(IMessageSender messageSender)
    {
        _messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender));
    }

    public void NotifyUser(string user, string message)
    {
        Console.WriteLine($"Notifying {user}...");
        _messageSender.SendMessage(message);
    }
}

接下来,你需要一个依赖注入容器来注册这些服务,并在需要时解析它们。在现代C#应用,特别是ASP.NET Core中,通常会使用内置的DI容器。

using Microsoft.Extensions.DependencyInjection;
using System;

public class Program
{
    public static void Main(string[] args)
    {
        // 4. 配置DI容器
        var services = new ServiceCollection();

        // 注册服务:将IMessageSender接口映射到EmailSender实现
        // 这里使用AddTransient,表示每次请求都创建一个新的实例
        services.AddTransient<IMessageSender, EmailSender>();
        services.AddTransient<NotificationService>(); // NotificationService本身也可能被注入

        // 构建服务提供者
        var serviceProvider = services.BuildServiceProvider();

        // 5. 从容器中获取实例(消费者)
        // 容器会自动解析NotificationService所依赖的IMessageSender
        var notificationService = serviceProvider.GetService<NotificationService>();

        // 使用服务
        notificationService.NotifyUser("Alice", "Your order has been shipped!");

        // 尝试获取另一个实例,会发现EmailSender也是新的(因为是Transient)
        var anotherNotificationService = serviceProvider.GetService<NotificationService>();
        anotherNotificationService.NotifyUser("Bob", "Your account balance is low.");
    }
}

这个流程展示了DI的核心思想:应用程序代码(

NotificationService
)不再负责创建其依赖(
IMessageSender
)的实例,而是由DI容器来负责。这让我们的代码更加松散耦合,也更容易测试。

为什么我们要用依赖注入?

依赖注入这东西,初看可能觉得有点绕,不就是多写了个接口,又多加了个容器吗?但一旦你深入体验过,就会发现它带来的好处是实实在在的。对我个人而言,DI最大的魅力在于它彻底改变了我们对“耦合”的看法。以前写代码,一个类要用另一个类,直接

new
一个就完事了,简单粗暴。但很快你会发现,当被依赖的类需要修改,或者你想换一个实现方式时,所有直接
new
它的地方都得改,这简直是噩梦。

DI解决了这个问题,它让你的代码变得“松散耦合”。我们不再直接依赖具体的实现,而是依赖抽象(接口)。这样一来,当你需要替换一个功能模块时,比如从邮件发送换成短信发送,你只需要写一个新的实现类,然后在DI容器里改一下注册配置就行了,原有的业务逻辑代码几乎不用动。这种可插拔性,对于大型项目或者需要频繁迭代的场景来说,简直是救命稻草。

再者,就是测试性。没有DI的时候,一个类如果依赖了数据库、外部API等,单元测试时就很难隔离,往往需要启动整个环境。有了DI,我们可以轻松地为接口创建Mock或Stub实现,在测试时注入这些假对象,从而实现真正的单元测试,让测试变得更快、更可靠。维护性、可扩展性这些就更不用说了,都是水到渠成的好处。它就像给你的代码装上了一套灵活的骨架,让它能够适应未来的变化,而不是僵硬地被当前的需求所束缚。

Fotor
Fotor

Fotor 在线照片编辑器

下载

依赖注入有哪些常见的实现方式?

在实践中,依赖注入主要有几种常见的实现模式,每种都有其适用场景,但也有各自的优缺点。理解它们能帮助你做出更明智的设计选择。

1. 构造函数注入 (Constructor Injection) 这是最推荐、最常用的方式。顾名思义,依赖项通过类的构造函数传入。

  • 优点:
    • 强制性依赖: 明确表示一个类必须依赖这些服务才能正常工作,如果缺少依赖,编译时或运行时就会报错,这比运行时才发现问题要好得多。
    • 不可变性: 依赖项在对象创建后就确定了,不能被修改,这有助于确保对象的内部状态一致性。
    • 易于测试: 构造函数直接暴露了所有依赖,使得在单元测试时很容易注入模拟对象。
    • 清晰的API: 从构造函数签名就能一目了然地看出一个类需要哪些外部服务。
  • 缺点:
    • 如果一个类有很多依赖,构造函数可能会变得很长,这被称为“构造函数爆炸”。这通常是“单一职责原则”被违反的信号,可能意味着这个类承担了过多的职责。

2. 属性注入 (Property Injection / Setter Injection) 通过公共属性(setter方法)来注入依赖。

  • 优点:
    • 可选性依赖: 适合注入那些并非每个实例都必需的依赖项。如果依赖是可选的,构造函数注入会导致构造函数参数过多。
    • 创建对象后注入: 可以在对象创建后动态地设置依赖。
  • 缺点:
    • 不明确的依赖: 从构造函数看不出有哪些依赖,需要查看属性才能发现。
    • 可变性: 依赖可以在对象生命周期内被更改,这可能导致一些不确定的行为。
    • 测试复杂性: 在测试时,需要确保所有必要的属性都被正确设置。
    • 可能导致对象在没有完全初始化的情况下被使用,因为依赖不是在构造时就保证存在的。

3. 方法注入 (Method Injection) 依赖项作为方法的参数传入。

  • 优点:
    • 特定场景: 适用于某个方法需要特定依赖,而这个依赖并非整个类都需要的情况。这能限制依赖的作用域
    • 瞬时依赖: 如果依赖只在某个方法执行时短暂需要,且每次调用可能不同,方法注入就很合适。
  • 缺点:
    • 参数过多: 如果一个方法需要很多依赖,参数列表会变得很长。
    • 不够通用: 不适合作为普遍的DI策略,因为它没有将依赖注入到整个对象中。

在我个人的开发实践中,我几乎总是优先选择构造函数注入。它强制你思考一个类的核心职责和它真正需要的依赖,如果构造函数变得臃肿,那往往是设计上需要调整的信号。属性注入我偶尔会用,但仅限于那些真正是“可选”的、或者是在特定框架(如某些ORM框架)中为了方便序列化或配置而不得不用的场景。方法注入则非常少用,通常只在一些非常具体、临时的功能中考虑。

在ASP.NET Core中,依赖注入是如何工作的?

ASP.NET Core在设计之初就把依赖注入作为其核心支柱之一,内置了一个非常强大且易于使用的DI容器。这意味着你在ASP.NET Core项目中,几乎不需要引入第三方DI库,就可以享受到DI带来的所有好处。

它的工作机制可以说相当优雅:

首先,所有的服务注册都集中在应用程序的启动阶段,具体来说,是在

Program.cs
(或旧版ASP.NET Core的
Startup.cs
中的
ConfigureServices
方法)里完成的。你通过
IServiceCollection
这个接口来注册各种服务,告诉容器“当有人需要
IMyService
的时候,请给他一个
MyServiceImplementation
的实例”。

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews(); // 比如注册MVC相关的服务

// 注册你的自定义服务
builder.Services.AddTransient<IMessageSender, EmailSender>(); // 每次请求都创建新实例
builder.Services.AddScoped<IUserRepository, UserRepository>(); // 每个HTTP请求创建一个实例
builder.Services.AddSingleton<ICacheService, MemoryCacheService>(); // 整个应用生命周期只创建一个实例

var app = builder.Build();

// ... 其他配置
app.Run();

这里面有几个关键的生命周期方法:

  • AddTransient<TService, TImplementation>()
    :瞬时(Transient)。每次从容器中请求该服务时,都会创建一个新的实例。这适用于轻量级、无状态的服务。
  • AddScoped<TService, TImplementation>()
    :作用域(Scoped)。在每个客户端请求(例如HTTP请求)的生命周期内,只创建一个实例。同一个请求内的所有地方都共享这个实例。这非常适合那些需要维护请求上下文状态的服务,比如数据库上下文。
  • AddSingleton<TService, TImplementation>()
    :单例(Singleton)。在整个应用程序的生命周期内,只创建一个实例。所有请求都共享这一个实例。适用于那些无状态、线程安全、资源消耗大的服务,比如日志记录器、配置管理器。

当ASP.NET Core应用程序运行时,DI容器会自动处理依赖的解析。比如,你的控制器(Controller)如果通过构造函数请求了

IMessageSender
,容器就会自动找到之前注册的
EmailSender
实例并注入进去。你不需要手动去
new
这些依赖,框架替你完成了这些繁琐的工作。

public class HomeController : Controller
{
    private readonly IMessageSender _messageSender;
    private readonly IUserRepository _userRepository;

    // ASP.NET Core的DI容器会自动解析并注入这些依赖
    public HomeController(IMessageSender messageSender, IUserRepository userRepository)
    {
        _messageSender = messageSender;
        _userRepository = userRepository;
    }

    public IActionResult Index()
    {
        _messageSender.SendMessage("Hello from Home Controller!");
        var user = _userRepository.GetUserById(1);
        ViewBag.UserName = user?.Name;
        return View();
    }
}

这种内置的DI机制极大地简化了ASP.NET Core应用的开发,让代码结构更清晰,也更容易进行测试和维护。当然,理解不同生命周期的含义非常重要,选错了生命周期可能会导致一些意想不到的问题,比如

Scoped
服务被注入到
Singleton
服务中,就可能出现“捕获依赖”的问题,因为
Singleton
服务会一直持有
Scoped
服务的引用,导致
Scoped
服务无法在请求结束时被正确释放。所以,在注册服务时,思考它们的生命周期是不可或缺的一步。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1977

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

681

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2414

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

49

2026.01.19

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

786

2023.08.10

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

391

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

2112

2023.08.14

vb怎么连接数据库
vb怎么连接数据库

在VB中,连接数据库通常使用ADO(ActiveX 数据对象)或 DAO(Data Access Objects)这两个技术来实现:1、引入ADO库;2、创建ADO连接对象;3、配置连接字符串;4、打开连接;5、执行SQL语句;6、处理查询结果;7、关闭连接即可。

359

2023.08.31

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

69

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
成为PHP架构师-自制PHP框架
成为PHP架构师-自制PHP框架

共28课时 | 2.6万人学习

Angular js入门篇
Angular js入门篇

共17课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号