周三下午,一个新入职的后端工程师在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后问题解决。
依赖注入不是炫技,是管理复杂度的基础设施。选对生命周期,比写一堆线程锁和清理代码更治本。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.