在C#开发中,依赖注入(DI)早已不是一项可选技术,而是整个.NET生态的核心基石。无论是Core框架自带的DI容器,还是诸如Autofac、Unity这样的第三方实现,它们的本质都是帮助我们管理对象依赖、实现项目解耦的「对象工厂」。
然而,不少开发者只熟悉框架封装好的用法,对其底层原理却一知半解。一旦遇到生命周期错乱或服务解析失败的问题,往往无从下手。今天,我们不依赖任何第三方库,从零开始手写一个轻量级的DI容器,再将其落地到实战项目中,旨在彻底吃透依赖注入的核心思想。
一、依赖注入的本质:为什么需要它?
1. 核心概念速览
- 依赖:当A类需要调用B类的方法来完成自身功能时,A就依赖于B。例如,控制器依赖服务层,服务层又依赖数据访问层。
- 注入:指不通过手动
new 来创建依赖对象,而是由外部容器自动传入(通常通过构造函数、属性或方法)。
- DI容器:一个统一管理所有服务的创建、生命周期和依赖解析的组件,你可以将其理解为一个智能的「对象管家」。
2. 传统方式的痛点:强耦合
让我们先看一个反面例子。在传统开发模式中,我们习惯于手动 new 对象,这导致类与类之间高度耦合:
// 仓储层:操作数据库
public class UserRepository
{
public void AddUser() => Console.WriteLine("新增用户");
}
// 服务层:依赖仓储层
public class UserService
{
// 硬编码new对象!强耦合:修改UserRepository就必须修改UserService
private readonly UserRepository _userRepo = new UserRepository();
public void AddUser() => _userRepo.AddUser();
}
// 调用层
var service = new UserService();
service.AddUser();
这种方式存在几个明显问题:
- 强耦合:如果
UserRepository 的类名或构造函数发生改变,UserService 也必须同步修改,牵一发而动全身。
- 难以测试:在单元测试中,我们无法轻松地将真实的
UserRepository 替换为模拟对象(Mock)。
- 管理混乱:对象的生命周期完全由开发者手动控制,容易导致内存泄漏或资源未释放。
3. 引入依赖注入的优势
- 解耦:依赖抽象(接口),而非具体实现,这是面向接口编程的核心实践。
- 自动管理:容器负责对象的创建与销毁,开发者无需关心
new 和 dispose 的细节。
- 易于测试:可以轻松为接口注入不同的实现(如Mock对象)进行单元测试。
- 统一规范:使项目结构更加清晰,更符合面向对象设计的开闭原则。
二、DI的前提:面向接口编程
依赖注入的核心思想是依赖抽象,而非具体实现。我们首先需要将上面的强耦合代码改造为面向接口的形式:
// 1. 定义抽象仓储接口
public interface IUserRepository
{
void AddUser();
}
// 2. 提供具体实现类
public class UserRepository : IUserRepository
{
public void AddUser() => Console.WriteLine("数据库新增用户");
}
// 3. 定义抽象服务接口
public interface IUserService
{
void AddUser();
}
// 4. 服务实现:构造函数依赖IUserRepository(抽象接口)
public class UserService : IUserService
{
private readonly IUserRepository _userRepo;
// 构造函数注入:依赖由外部传入,内部不自己new
public UserService(IUserRepository userRepo)
{
_userRepo = userRepo;
}
public void AddUser() => _userRepo.AddUser();
}
现在,UserService 只认识 IUserRepository 这个接口,完全不知道也不关心背后具体是哪个实现类。这就是解耦的第一步。
三、从零手写C# DI容器:理解核心原理
一个最精简的DI容器,只需要完成三件核心任务:
- 注册服务:绑定「抽象接口」与「具体实现类」,并声明其生命周期。
- 解析服务:根据请求的抽象类型,自动创建对象,并递归地解析其所有依赖。
- 管理生命周期:控制服务实例是单例、瞬时还是作用域内唯一。
1. 定义生命周期枚举
参照.NET Core的标准,我们定义三种常见的生命周期:
/// <summary>
/// 服务生命周期
/// </summary>
public enum ServiceLifetime
{
Singleton, // 单例:全局唯一实例
Transient, // 瞬时:每次获取都创建新实例
Scoped // 作用域:每个请求/作用域内一个实例(Web常用)
}
2. 定义服务描述实体
这个类用于存储注册服务时的所有元数据信息:
/// <summary>
/// 服务描述:存储接口与实现的映射关系
/// </summary>
public class ServiceDescriptor
{
public Type ServiceType { get; } // 抽象类型(接口)
public Type ImplementationType { get; } // 具体实现类型
public ServiceLifetime Lifetime { get; } // 生命周期
public object? ImplementationInstance { get; set; } // 单例实例缓存
// 构造函数
public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime)
{
ServiceType = serviceType;
ImplementationType = implementationType;
Lifetime = lifetime;
}
}
3. 实现DI容器核心类
这是整个容器的核心,包含服务注册和递归解析依赖的逻辑:
/// <summary>
/// 手写轻量级DI容器
/// </summary>
public class MiniContainer
{
// 存储所有注册的服务描述
private readonly List<ServiceDescriptor> _services = new();
#region 1. 服务注册方法
// 注册单例服务
public void AddSingleton<TService, TImplementation>() where TImplementation : TService
{
_services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), ServiceLifetime.Singleton));
}
// 注册瞬时服务
public void AddTransient<TService, TImplementation>() where TImplementation : TService
{
_services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), ServiceLifetime.Transient));
}
#endregion
#region 2. 服务解析核心方法
/// <summary>
/// 根据接口类型获取服务实例
/// </summary>
public TService GetService<TService>()
{
return (TService)GetService(typeof(TService));
}
/// <summary>
/// 递归解析服务:自动创建对象+注入依赖
/// </summary>
private object GetService(Type serviceType)
{
// 1. 找到注册的服务描述
var descriptor = _services.FirstOrDefault(s => s.ServiceType == serviceType);
if (descriptor == null)
throw new Exception($"未注册服务:{serviceType.Name}");
// 2. 如果是单例且已创建,直接返回缓存实例
if (descriptor.Lifetime == ServiceLifetime.Singleton && descriptor.ImplementationInstance != null)
return descriptor.ImplementationInstance;
// 3. 找到实现类的第一个构造函数
var constructor = descriptor.ImplementationType.GetConstructors().First();
// 4. 递归解析构造函数的所有参数依赖
var parameters = constructor.GetParameters();
var parameterInstances = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
// 递归调用:自动创建参数对象
parameterInstances[i] = GetService(parameters[i].ParameterType);
}
// 5. 利用反射创建服务实例
var instance = Activator.CreateInstance(descriptor.ImplementationType, parameterInstances);
// 6. 如果是单例,则缓存实例
if (descriptor.Lifetime == ServiceLifetime.Singleton)
descriptor.ImplementationInstance = instance;
return instance;
}
#endregion
}
4. 测试我们手写的容器
现在,使用我们刚实现的 MiniContainer 来运行之前的示例:
class Program
{
static void Main(string[] args)
{
// 1. 创建DI容器
var container = new MiniContainer();
// 2. 注册服务:接口→实现+生命周期
container.AddSingleton<IUserRepository, UserRepository>();
container.AddTransient<IUserService, UserService>();
// 3. 解析服务:容器自动创建对象+注入依赖
var userService = container.GetService<IUserService>();
userService.AddUser(); // 输出:数据库新增用户
// 验证:瞬时服务每次创建新对象
var userService2 = container.GetService<IUserService>();
Console.WriteLine(object.ReferenceEquals(userService, userService2)); // 输出:False
}
}
运行结果:
数据库新增用户
False
✅ 成功!我们没有在任何地方手动 new UserRepository() 或 new UserService(),容器自动完成了依赖的解析和注入,完美实现了代码解耦。
四、回归实战:.NET 原生DI容器应用
理解原理后,在实际开发中,我们直接使用功能更完善、经过充分测试的微软官方DI容器(ASP.NET Core内置,也可用于控制台程序)。
1. 三种生命周期及使用场景
| 注册方法 |
生命周期 |
典型使用场景 |
AddSingleton |
单例 |
工具类、配置类、全局共享的缓存服务 |
AddScoped |
作用域 |
Web请求上下文、数据库上下文(DbContext) |
AddTransient |
瞬时 |
轻量级、无状态的服务 |
2. 在控制台项目中使用
首先,通过NuGet安装必要的包:
Install-Package Microsoft.Extensions.DependencyInjection
然后,编写使用代码:
using Microsoft.Extensions.DependencyInjection;
class Program
{
static void Main(string[] args)
{
// 1. 创建服务集合(即DI容器配置)
var services = new ServiceCollection();
// 2. 注册服务
services.AddSingleton<IUserRepository, UserRepository>();
services.AddTransient<IUserService, UserService>();
// 3. 构建服务提供程序(即容器本体)
var serviceProvider = services.BuildServiceProvider();
// 4. 解析并使用服务
var userService = serviceProvider.GetRequiredService<IUserService>();
userService.AddUser();
}
}
3. 在ASP.NET Core中使用(最常用场景)
在 Program.cs 中,注册服务变得异常简单:
var builder = WebApplication.CreateBuilder(args);
// 注册服务:一行代码完成解耦配置
builder.Services.AddSingleton<IUserRepository, UserRepository>();
builder.Services.AddTransient<IUserService, UserService>();
var app = builder.Build();
app.Run();
在控制器中,框架会自动通过构造函数完成依赖注入:
public class UserController : Controller
{
private readonly IUserService _userService;
// 框架自动注入已注册的IUserService实现,无需手动new
public UserController(IUserService userService)
{
_userService = userService;
}
public IActionResult Add()
{
_userService.AddUser();
return Ok("新增成功");
}
}
五、核心总结与避坑指南
核心精髓
- 依赖抽象,而非具体:用接口定义行为,用实现类完成功能,这是解耦的基石。
- 容器管理,解放双手:遵循“注册-解析”两步走模式,对象的创建和依赖注入由容器自动完成。
- 生命周期是关键:深刻理解单例(Singleton)、作用域(Scoped)和瞬时(Transient)的区别与应用场景,是正确使用DI的保证。
开发避坑指南
- 避免生命周期错乱:切勿在单例服务中注入瞬时或作用域服务,这可能导致后者被意外提升为单例,引发内存泄漏或状态混乱。
- 首选构造函数注入:这是最明确、最安全的注入方式。属性注入和方法注入应仅作为补充手段在特定场景下使用。
- 先注册,后解析:所有服务必须在
BuildServiceProvider() 之前完成注册,否则解析时会抛出 InvalidOperationException。
- DbContext使用Scoped:在ASP.NET Core中,Entity Framework Core的
DbContext 应使用 AddScoped 注册,以确保在一个Web请求内使用同一个连接实例。
结语
通过从零手写一个DI容器,我们深入剖析了其“注册映射、递归解析、生命周期管理”三大核心机制。依赖注入并非一种复杂晦涩的设计模式,而是一种能让代码变得更优雅、更易于维护和测试的最佳工程实践。
在C#与.NET开发中,熟练掌握依赖注入是迈向高质量架构设计的第一步。希望本文的讲解与实战,能帮助你彻底告别“硬编码 new 对象”的强耦合时代,写出更加清晰、灵活且健壮的应用。