依赖注入(Dependency Injection,简称DI)是控制反转(IOC)思想的一种具体实现方式,核心是将对象的依赖关系由外部容器来管理和注入,而不是在对象内部主动创建依赖实例,从而减少代码之间的耦合程度。在C#的生态中,尤其是.Net Core及之后的框架,依赖注入已经成为内置的核心功能,几乎所有层级的服务都可以通过依赖注入来管理。

依赖注入的核心概念
依赖注入主要包含三个角色:服务(被依赖的对象)、服务容器(管理服务的注册和生命周期的容器)、消费者(需要使用服务的对象)。它的核心优势在于:
- 降低耦合:消费者不需要关心依赖的创建细节,只需要定义依赖的接口或类型即可
- 提升可测试性:可以很方便地替换依赖的实现,比如测试时使用模拟对象
- 统一管理生命周期:服务容器可以统一控制依赖的创建、销毁时机
C#项目中配置依赖注入的步骤
1. 定义服务接口和实现
首先我们需要定义要注册的服务,通常先定义接口,再提供具体实现,这样更符合面向接口编程的原则。
// 定义服务接口
public interface IUserService
{
string GetUserName(int userId);
}
// 定义服务实现
public class UserService : IUserService
{
public string GetUserName(int userId)
{
// 模拟从数据库获取用户名称的逻辑
return $"用户{userId}";
}
}
2. 注册服务到容器
在.Net Core及之后的项目中,服务的注册通常在程序的启动阶段完成,也就是Program.cs文件中的WebApplicationBuilder的Services属性上进行操作。服务的注册需要指定服务的生命周期,C#依赖注入内置了三种常见的生命周期:
| 生命周期类型 | 说明 |
|---|---|
| Transient | 每次请求服务时都会创建一个新的实例 |
| Scoped | 在同一个请求范围内,多次获取服务会得到同一个实例,不同请求之间是不同实例 |
| Singleton | 整个应用程序生命周期内,只会创建一个实例,所有请求共享该实例 |
以下是注册服务的代码示例:
var builder = WebApplication.CreateBuilder(args); // 注册瞬态服务,每次获取都会创建新实例 builder.Services.AddTransient<IUserService, UserService>(); // 注册作用域服务,同请求内实例相同 // builder.Services.AddScoped<IUserService, UserService>(); // 注册单例服务,全局唯一实例 // builder.Services.AddSingleton<IUserService, UserService>(); var app = builder.Build();
3. 在消费者中注入服务
服务注册完成之后,就可以在需要使用服务的类中通过构造函数注入的方式获取服务实例,框架会自动从容器中找到对应的实现并注入。
// 控制器中注入服务示例
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
// 构造函数注入,框架自动传入IUserService的实现实例
public UserController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public string Get(int id)
{
return _userService.GetUserName(id);
}
}
如果是普通的非控制器类,只要该类本身也是通过依赖注入创建的,同样可以通过构造函数注入服务:
public class OrderService
{
private readonly IUserService _userService;
public OrderService(IUserService userService)
{
_userService = userService;
}
public string CreateOrder(int userId)
{
var userName = _userService.GetUserName(userId);
return $"为用户{userName}创建订单";
}
}
4. 手动获取服务(可选场景)
大部分场景下我们不需要手动获取服务,但是在一些特殊场景,比如需要在静态方法中获取服务,或者无法通过构造函数注入的场景,可以通过IServiceProvider来手动获取服务:
// 手动获取服务示例
public class ServiceHelper
{
private readonly IServiceProvider _serviceProvider;
public ServiceHelper(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public string GetUserNameManual(int userId)
{
// 从服务容器中获取IUserService实例
using var scope = _serviceProvider.CreateScope();
var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
return userService.GetUserName(userId);
}
}
注意事项
在配置依赖注入时需要注意几个问题:首先,注册的服务类型必须和注入时使用的类型匹配,否则会抛出找不到服务的异常;其次,要根据实际业务场景选择合适的生命周期,比如如果服务中持有数据库连接等需要释放的资源,不建议使用Singleton生命周期;最后,避免循环依赖,也就是A依赖B,B又依赖A的情况,这种情况会导致服务无法正确创建。