周三下午,一个新入职的后端工程师在Code Review时问我:为什么同样的接口,有时候注入的是同一个对象,有时候又不是?这个问题背后,其实是ASP.NET Core依赖注入容器里最基础也最容易踩坑的三个生命周期模式——Singleton、Scoped和Transient。搞懂它们的区别,能帮你避开80%的内存泄漏和并发问题。

我用一段极简代码做了实测,把三种模式的行为差异彻底摊开在桌面上。

打开网易新闻 查看精彩图片

测试设计很直接:一个生成随机ID的服务,一个调用两次该服务的控制器,观察实例ID的变化规律。

打开网易新闻 查看精彩图片

Singleton(单例模式)——全局唯一

注册方式:builder.Services.AddSingleton();

实测输出显示,三次API请求的实例ID都是178。构造函数只在第一次请求时执行了一次,后续所有请求都复用同一个对象。

这意味着什么?整个应用程序生命周期内,内存中只有一份实例。适合无状态、线程安全的服务,比如配置读取器、缓存管理器。但要小心——如果里面存了请求相关的数据,或者不是线程安全的,就会出大乱子。

Scoped(作用域模式)——请求内单例,请求间隔离

注册方式:builder.Services.AddScoped();

三次请求的实例ID分别是438、674、365。同一个请求内两次调用GetService拿到的是同一个对象,但不同请求之间完全隔离。

这是Web API的默认选择。数据库上下文(DbContext)就是典型场景——一个请求一个连接,请求结束自动释放,既避免了连接池耗尽,又防止了跨请求的数据混乱。

Transient(瞬态模式)——每次新建

打开网易新闻 查看精彩图片

注册方式:builder.Services.AddTransient();

输出最直观:同一个请求里两次调用,实例ID分别是794和182,两次都走了构造函数。每次解析都创建新实例,没有任何共享。

适合轻量级、无依赖的服务,或者需要完全隔离状态的场景。代价是GC压力——高频调用时对象创建和销毁的开销不可忽视。

三种模式的核心差异

用一张表说清:

模式实例创建时机共享范围典型风险 Singleton首次解析时全局内存泄漏、线程安全问题 Scoped作用域开始时单个请求跨作用域误用导致数据混乱 Transient每次解析时不共享高频创建的性能开销

实测代码里有个细节值得注意:Scoped模式下,同一个请求内多次解析返回同一实例,这是依赖注入容器自动做的缓存。如果你手动new一个作用域(CreateScope),里面又是另一套隔离规则。

另一个常见坑是服务之间的依赖关系。一个Singleton服务注入了Scoped服务会发生什么?容器会抛异常,因为这会导致Scoped服务被"提升"为事实上的单例,破坏了生命周期约定。

回到开头那个Code Review的问题。代码里混用了不同生命周期的服务,导致某些请求拿到了"脏数据"——前一个请求残留的状态。改成Scoped后问题解决。

依赖注入不是炫技,是管理复杂度的基础设施。选对生命周期,比写一堆线程锁和清理代码更治本。