.NET 依赖注入容器使用心得
依赖注入是 .NET Core 之后的核心特性,但三种生命周期经常搞混。刚开始学的时候觉得"不就是在构造函数里加个参数吗",真遇到运行时报错才发现生命周期没选对会出各种奇怪问题。这篇笔记整理一下自己的理解。
一、三种生命周期
| 生命周期 | 注册方法 | 实例数量 |
|---|---|---|
| Singleton 单例 | AddSingleton | 整个应用只有一个实例 |
| Scoped 作用域 | AddScoped | 每个请求一个实例 |
| Transient 瞬时 | AddTransient | 每次请求都新建 |
二、注册写法
var builder = WebApplication.CreateBuilder(args); // 接口 + 实现 builder.Services.AddSingleton<ILogger, ConsoleLogger>(); builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddTransient<INotification, EmailNotification>();
三、常见误区
- 把 Scoped 服务注入到 Singleton:容器会直接报错,因为这相当于把一个请求级别的对象提升成全局单例,会导致状态错乱。
- 把有状态的对象注册成 Singleton:如果服务里有可变字段,多线程下会出问题。
- 所有服务都用 Transient:虽然最安全,但每次都 new 会有性能开销,数据库上下文这种一般用 Scoped。
四、数据库上下文的例子
EF Core 的 DbContext 默认注册为 Scoped,因为它内部维护了一个工作单元(Unit of Work),应该在一个请求结束时释放:
builder.Services.AddDbContext<AppDbContext>(
options => options.UseSqlServer(connString));
这也是为什么不能把 DbContext 注入到 Singleton 服务里——一个请求里改的数据还没保存,下一个请求就拿到了旧实例。这个报错信息第一次看到时完全没反应过来,后来查了资料才明白是生命周期不匹配。
五、构造函数注入
正确做法是在构造函数里声明依赖,容器会自动传进来:
public class OrderService
{
private readonly IRepository _repo;
public OrderService(IRepository repo)
{
_repo = repo; // 容器自动注入
}
}
这样写的好处是:依赖关系一目了然,单元测试时也可以方便地替换成 mock 实现。不要在服务里自己 new 依赖对象,那样就失去了 DI 的意义。