【C#/.NET 进阶】ASP.NET 架构与最佳实践:DI 依赖注入(IoC 核心)从入门到避坑

目录

    • [一、先搞懂:什么是 DI / IoC?(核心思想)](#一、先搞懂:什么是 DI / IoC?(核心思想))
      • [1.1 官方定义(简化版)](#1.1 官方定义(简化版))
      • [1.2 生活类比:点外卖 vs 自己做饭](#1.2 生活类比:点外卖 vs 自己做饭)
      • 核心结论:
      • [1.3 DI 核心好处(可维护架构必备)](#1.3 DI 核心好处(可维护架构必备))
    • [二、流程图:ASP.NET Core DI 完整工作流程](#二、流程图:ASP.NET Core DI 完整工作流程)
    • [三、代码实战:从 "烂代码" 到 "DI 最佳实践"](#三、代码实战:从 “烂代码” 到 “DI 最佳实践”)
      • [3.1 反例:不使用 DI(高度耦合,禁止在项目中使用)](#3.1 反例:不使用 DI(高度耦合,禁止在项目中使用))
      • [3.2 正例:使用 DI(ASP.NET Core 官方最佳实践)](#3.2 正例:使用 DI(ASP.NET Core 官方最佳实践))
        • [步骤 1:定义接口(面向接口编程,DI 核心规范)](#步骤 1:定义接口(面向接口编程,DI 核心规范))
        • [步骤 2:定义业务服务,通过构造函数注入依赖](#步骤 2:定义业务服务,通过构造函数注入依赖)
        • [步骤 3:在 Program.cs 中注册服务(最重要一步)](#步骤 3:在 Program.cs 中注册服务(最重要一步))
        • [步骤 4:控制器注入使用](#步骤 4:控制器注入使用)
    • [四、ASP.NET Core DI 三种生命周期(必考 + 必掌握)](#四、ASP.NET Core DI 三种生命周期(必考 + 必掌握))
      • [4.1 三种生命周期对比](#4.1 三种生命周期对比)
      • [4.2 代码注册方式](#4.2 代码注册方式)
    • [五、DI 注入的 3 种方式(官方推荐优先级)](#五、DI 注入的 3 种方式(官方推荐优先级))
      • [1. 构造函数注入(官方唯一推荐 ✅)](#1. 构造函数注入(官方唯一推荐 ✅))
      • [2. 属性注入(不推荐 ⚠️)](#2. 属性注入(不推荐 ⚠️))
      • [3. 方法注入(极少用 🚫)](#3. 方法注入(极少用 🚫))
    • [六、新手常踩的 7 个大坑(90% 开发者都中过)](#六、新手常踩的 7 个大坑(90% 开发者都中过))
      • [坑 1:没有注册服务 → 直接报 InvalidOperationException](#坑 1:没有注册服务 → 直接报 InvalidOperationException)
      • [坑 2:生命周期错误(最常见)](#坑 2:生命周期错误(最常见))
      • [坑 3:手动 new 服务,还希望 DI 注入](#坑 3:手动 new 服务,还希望 DI 注入)
      • [坑 4:构造函数注入为 null,直接空指针](#坑 4:构造函数注入为 null,直接空指针)
      • [坑 5:循环依赖(A 注入 B,B 注入 A)](#坑 5:循环依赖(A 注入 B,B 注入 A))
      • [坑 6:滥用 Singleton 导致线程安全问题](#坑 6:滥用 Singleton 导致线程安全问题)
      • [坑 7:服务没有实现接口,直接注册类(不规范)](#坑 7:服务没有实现接口,直接注册类(不规范))
    • [七、企业级 DI 编码规范(可维护性必备)](#七、企业级 DI 编码规范(可维护性必备))
    • 八、总结

开启 ASP.NET 架构规范 + 可维护性 专栏第一期:依赖注入(DI) 。这是 ASP.NET Core 最核心、最基础、面试必问、项目必用的技术,也是写出松耦合、可测试、易维护 代码的基石。

很多新手觉得 DI 很抽象,其实它本质就一句话:自己不 new 对象,交给框架去管理 。我会用生活类比 + 完整代码 + 流程图 + 避坑指南,把 DI 讲得明明白白。

一、先搞懂:什么是 DI / IoC?(核心思想)

1.1 官方定义(简化版)

  • IoC(Inversion of Control)控制反转:把对象的创建、生命周期管理,从 "自己手动控制" 反转给 "框架自动控制"
  • DI(Dependency Injection)依赖注入:IoC 的具体实现方式 ------ 框架自动把需要的对象 "注入" 到类里,不用自己实例化。

1.2 生活类比:点外卖 vs 自己做饭

  • 不使用 DI(传统写法) :自己买菜、切菜、炒菜、洗碗 = 自己 new 对象、管理对象、销毁对象,累、耦合高、换做法麻烦。
  • 使用 DI(最佳实践) :打开外卖软件,点单,饭直接送到家 = 告诉框架我需要什么对象,框架直接给你,不用管怎么创建。

核心结论:

DI 就是 "不用自己 new,框架自动给",实现代码解耦,提升可维护性。

1.3 DI 核心好处(可维护架构必备)

1.松耦合 :类与类之间不硬编码依赖,替换功能不用改大量代码

2.可测试性 :单元测试可以轻松替换 mock 对象

3.统一管理对象 :框架控制生命周期,避免内存泄漏

3.代码规范整洁:符合单一职责,可读性极高

二、流程图:ASP.NET Core DI 完整工作流程

我用最清晰的流程告诉你,DI 在框架里到底怎么跑的:
定义服务接口/类
Program.cs 注册服务
框架创建 ServiceProvider 容器
控制器/类声明构造函数依赖
框架自动从容器获取对象
自动注入到使用方
按照生命周期管理对象

一句话流程:

定义 → 注册 → 容器管理 → 声明依赖 → 自动注入 → 生命周期管理

三、代码实战:从 "烂代码" 到 "DI 最佳实践"

我们用一个最常见的业务场景:用户服务 + 日志记录

3.1 反例:不使用 DI(高度耦合,禁止在项目中使用)

csharp 复制代码
// 日志类
public class MyLogger
{
    public void Log(string msg) => Console.WriteLine($"日志:{msg}");
}

// 用户服务
public class UserService
{
    // 自己 new 对象!高度耦合!
    private readonly MyLogger _logger = new MyLogger();

    public void AddUser()
    {
        _logger.Log("添加用户成功");
        Console.WriteLine("执行添加用户逻辑");
    }
}

// 控制器
public class UserController : Controller
{
    private readonly UserService _userService = new UserService();

    public IActionResult Add()
    {
        _userService.AddUser();
        return Ok();
    }
}

问题:

  • 高度耦合,MyLogger 一改,所有地方都要改
  • 无法单元测试
  • 手动 new 对象,无法控制生命周期
  • 完全不符合架构规范

3.2 正例:使用 DI(ASP.NET Core 官方最佳实践)

步骤 1:定义接口(面向接口编程,DI 核心规范)
csharp 复制代码
/// <summary>
/// 日志接口(面向接口,不面向实现)
/// </summary>
public interface ILogger
{
    void Log(string message);
}

/// <summary>
/// 日志实现类
/// </summary>
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[DI 注入日志] {message}");
    }
}
步骤 2:定义业务服务,通过构造函数注入依赖
csharp 复制代码
/// <summary>
/// 用户服务
/// </summary>
public interface IUserService
{
    void AddUser();
}

public class UserService : IUserService
{
    // 构造函数注入:DI 最标准、最常用的方式
    private readonly ILogger _logger;

    // 关键:自己不 new,框架自动传进来
    public UserService(ILogger logger)
    {
        _logger = logger;
    }

    public void AddUser()
    {
        _logger.Log("用户新增成功");
        Console.WriteLine("UserService:执行业务逻辑");
    }
}
步骤 3:在 Program.cs 中注册服务(最重要一步)
csharp 复制代码
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

// ------------ DI 服务注册 ------------
// 注册日志
builder.Services.AddScoped<ILogger, ConsoleLogger>();
// 注册用户服务
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();
步骤 4:控制器注入使用
csharp 复制代码
public class UserController : Controller
{
    private readonly IUserService _userService;

    // 控制器也通过构造函数注入
    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    public IActionResult Index()
    {
        _userService.AddUser();
        return View();
    }
}

运行结果:

plaintext 复制代码
[DI 注入日志] 用户新增成功
UserService:执行业务逻辑

✅** 完美实现:无耦合、可维护、可测试、架构规范**

四、ASP.NET Core DI 三种生命周期(必考 + 必掌握)

DI 最重要知识点之一:对象存活时间,注册错会导致 Bug、内存泄漏、线程安全问题。

4.1 三种生命周期对比

表格

类型 全称 中文 创建时机 核心特点 使用场景
Singleton 单例 全局唯一 程序启动一次 整个应用只有一个实例 配置、日志、工具类
Scoped 作用域 一次请求 每次请求创建 请求内唯一,请求结束销毁 业务服务、Repository、工作单元
Transient 瞬时 用完即毁 每次使用都创建 每次注入都新建实例 轻量级无状态服务

4.2 代码注册方式

csharp 复制代码
// 单例
builder.Services.AddSingleton<ILogger, ConsoleLogger>();

// 作用域(最常用)
builder.Services.AddScoped<IUserService, UserService>();

// 瞬时
builder.Services.AddTransient<ITempService, TempService>();

企业规范:
默认优先使用 Scoped,Singleton 谨慎用(线程不安全),Transient 用于轻量级服务。

五、DI 注入的 3 种方式(官方推荐优先级)

ASP.NET Core 支持 3 种注入方式,****企业规范只推荐第一种。

1. 构造函数注入(官方唯一推荐 ✅)

csharp 复制代码
public class UserService
{
    private readonly ILogger _logger;

    // 依赖必须,必须注入
    public UserService(ILogger logger)
    {
        _logger = logger;
    }
}

优点:依赖明确、强制检查、线程安全、测试方便

2. 属性注入(不推荐 ⚠️)

csharp 复制代码
public ILogger Logger { get; set; }

缺点:容易为 null、依赖不明确、破坏封装

3. 方法注入(极少用 🚫)

仅用于特殊场景,普通业务绝对不要用
企业最佳实践:只使用构造函数注入,禁止属性注入!

六、新手常踩的 7 个大坑(90% 开发者都中过)

这一节价值千金,帮你避开项目中 99% 的 DI 异常。

坑 1:没有注册服务 → 直接报 InvalidOperationException

plaintext 复制代码
InvalidOperationException: Unable to resolve service for type 'ILogger'

原因: 只写了接口,没在 Program.cs 注册
解决: 必须 AddScoped/AddSingleton 注册

坑 2:生命周期错误(最常见)

  • 单例(Singleton)注入 作用域(Scoped)服务 → 程序直接崩溃
  • 原因:长生命周期引用短生命周期,会导致对象已释放
    规范 :生命周期只能同级或向下引用
    • Singleton 可引用:Singleton
    • Scoped 可引用:Scoped、Singleton、Transient
    • Transient 可引用:全部

坑 3:手动 new 服务,还希望 DI 注入

csharp 复制代码
// 错误!自己 new 的对象,框架不会帮你注入依赖
var userService = new UserService();

**解决:**所有需要 DI 的对象,必须交给框架管理,禁止手动 new

坑 4:构造函数注入为 null,直接空指针

原因 :没注册 + 循环依赖
解决:检查注册、检查循环依赖

坑 5:循环依赖(A 注入 B,B 注入 A)

plaintext 复制代码
A -> B -> A

解决:拆分服务,使用 Lazy 或中介者模式,解除循环

坑 6:滥用 Singleton 导致线程安全问题

原因 :单例全局唯一,多线程同时修改会脏数据
规范:单例只存无状态、只读对象

坑 7:服务没有实现接口,直接注册类(不规范)

不推荐:

csharp 复制代码
builder.Services.AddScoped<ConsoleLogger>();

推荐:

csharp 复制代码
builder.Services.AddScoped<ILogger, ConsoleLogger>();

原因:面向接口编程,方便替换、测试、解耦

七、企业级 DI 编码规范(可维护性必备)

我把多年大厂架构规范总结成 6 条:

1.**面向接口编程:**所有业务服务必须定义接口

2.只使用构造函数注入 ,禁止属性注入

3.默认使用 Scoped 生命周期

4.服务必须在 Program.cs 集中注册

5.禁止在服务中手动 new 其他依赖服务

6.单例服务必须保证线程安全

遵守这套规范,你的代码会:

新人一看就懂

  • 维护成本极低
  • 单元测试轻松写
  • 架构完全符合 ASP.NET Core 设计哲学

八、总结

DI 依赖注入是 ASP.NET Core 架构的灵魂

  • IoC 是思想:把对象控制权交给框架
  • DI 是实现:自动注入依赖,不用自己 new
  • 三种生命周期:Singleton / Scoped / Transient
  • 一种最佳注入方式:构造函数注入
  • 核心目标:松耦合、可测试、可维护
    学会 DI,你才算真正入门 ASP.NET Core 架构设计。

欢迎在评论区留言:你踩过 DI 最大的坑是什么?

我会一一回复

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,我们下期继续深入 ASP.NET 架构最佳实践!

相关推荐
武藤一雄8 小时前
WPF:MessageBox系统消息框
前端·microsoft·c#·.net·wpf
武藤一雄8 小时前
WPF进阶:万字详解WPF如何性能优化
windows·性能优化·c#·.net·wpf·.netcore·鲁棒性
xiaogutou11211 天前
2026年历史课件PPT模板选购指南:教师备课效率与精度的平衡方案
开发语言·c#
Eiceblue1 天前
使用 C# 将 Excel 转换为 Markdown 表格(含批量转换示例)
开发语言·c#·excel
不会编程的懒洋洋1 天前
WPF XAML+布局+控件
xml·开发语言·c#·视觉检测·wpf·机器视觉·视图
唐青枫1 天前
别再层层传参了!C#.NET AsyncLocal 异步上下文透传实战
c#·.net
明如正午1 天前
【C#】托管调试助手 “PInvokeStackImbalance“:的调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。
c#
Eiceblue1 天前
C# 如何实现 Word 转 Excel ?分享两种实用方法
c#·word·excel
天才少女爱迪生1 天前
word格式规范检测+自动修改【python】
python·c#·word