AddTransient、AddScoped 和 AddSingleton:如何不混淆 C# 服务生命周期
\n\n您是否曾有过这样的想法:“为什么我的服务在请求之间保存了数据,尽管我并没有要求它这样做?”或者相反——“为什么每次都创建一个新对象,导致我丢失了状态?”
\n\n欢迎来到 ASP.NET Core 中依赖注入 (Dependency Injection, DI) 的世界!这是初学者最强大但也最容易混淆的主题之一。今天,我们将剖析支撑 C# 对象管理的三大支柱:AddTransient、AddScoped 和 AddSingleton。
到本文结束时,您不仅会理解它们的区别,还能在面试中自信地回答这个问题(是的,面试官超爱问这个!)。我们开始吧!
\n\n什么是服务生命周期,为什么需要它?
\n\n想象一下,您的应用程序是一家餐厅。服务就是厨师、服务员和餐具。生命周期决定了它们“存活”多久:
\n\n- \n
- Transient(瞬态) — 一次性员工。来了,做了一碗汤就走了。新订单来,换新员工。 \n
- Scoped(作用域) — 一个班次的员工。在餐厅营业期间(即 HTTP 请求持续期间)为所有顾客服务。 \n
- Singleton(单例) — 主厨,在餐厅的整个生命周期内工作。整个应用程序只有一个。 \n
在 C# 中,此逻辑通过内置的 DI 容器实现。让我们看看代码。
\n\n1. AddTransient — 每次都创建新实例
\n\n何时使用: 无状态 (stateless) 的轻量级服务,不在调用之间存储数据。非常适合仓库、辅助类、工具类。
\n\n工作原理: 每次您请求服务时(通过构造函数或直接从容器中),都会创建一个新对象。
\n\n// 在 Program.cs 中注册\nbuilder.Services.AddTransient<IMyService, MyService>();\n\n// 服务示例\npublic interface IMyService\n{\n Guid GetInstanceId();\n}\n\npublic class MyService : IMyService\n{\n private readonly Guid _id = Guid.NewGuid();\n public Guid GetInstanceId() => _id;\n}\n\n// 使用该服务的控制器\npublic class HomeController : Controller\n{\n private readonly IMyService _service1;\n private readonly IMyService _service2;\n\n public HomeController(IMyService service1, IMyService service2)\n {\n _service1 = service1;\n _service2 = service2;\n }\n\n public IActionResult Index()\n {\n ViewBag.Id1 = _service1.GetInstanceId();\n ViewBag.Id2 = _service2.GetInstanceId();\n // Id1 和 Id2 是不同的 GUID!\n return View();\n }\n}\n\n\n重要提示: 即使在同一个 HTTP 请求中,如果您在两个不同的地方注入 IMyService,您将得到两个不同的对象。这会消耗内存,但不会有数据“污染”的风险。
2. AddScoped — 每个请求一个实例
\n\n何时使用: 需要在单个 HTTP 请求内保持一致的服务。例如,Entity Framework Core 中的 DbContext、购物车服务、请求日志记录器。
工作原理: 在单个 HTTP 请求(或作用域)内,创建一个实例。如果您多次请求它,您将得到同一个对象。但来自其他用户的每个新请求都会获得自己的实例。
\n\n// 注册\nbuilder.Services.AddScoped<IScopedService, ScopedService>();\n\npublic interface IScopedService\n{\n Guid GetInstanceId();\n}\n\npublic class ScopedService : IScopedService\n{\n private readonly Guid _id = Guid.NewGuid();\n public Guid GetInstanceId() => _id;\n}\n\n// 在同一个控制器中\npublic