.NET

.NET Core 中 Service 的生命週期 (Transient, Scoped, Singleton) 及使用時機

Transient, Scoped, Singleton 的區別

Transient 跟 Singleton 比較直觀,Transient 是「永遠創建新的實體 (instance)」,而 Singleton 剛好相反,是「只創建一次實體且永遠共享」;Scoped 則是以請求 (request) 為分界,同一個請求內只會創建一個共享的實體,但不同請求則會創建不同實體。可參考 Stack Overflow 上的說明:

Web API 範例

來看個範例會更好理解(完整原始碼可以從 GitHub 下載),首先創建一個 Web API 的範例專案

# 我電腦上的 dotnet 版本是 6.0
C:\Users\Jim>dotnet --version
6.0.202
C:\Users\Jim>dotnet new webapi -o ObjLifeCycle
The template "ASP.NET Core Web API" was created successfully.
...

接著建立新目錄/檔案 Interfaces/DemoClass.cs,並創造一些空的 interfaces 方便識別

# Interfaces/DemoClass.cs
namespace LifeCycleDemo.Interface;
public interface IScoped { }
public interface ISingleton { }
public interface ITransient { }
public class DemoClass : IScoped, ISingleton, ITransient { }

再建立一個新目錄/檔案 Services/DemoService.cs,在 constructor 中注入三種 interface 的實體,並且提供 GetServiceHashCode(),回傳其 data member 的 hash code (物件識別碼)

# Services/DemoService.cs
using LifeCycleDemo.Interface;
namespace LifeCycleDemo.Services;
public class DemoService 
{
    private readonly ITransient _transient;
    private readonly IScoped _scoped;
    private readonly ISingleton _singleton;
    public DemoService(ITransient transient, IScoped scoped, ISingleton singleton) 
    {
        _transient = transient;
        _scoped = scoped;
        _singleton = singleton;
    }
    public string GetServiceHashCode() 
    {
        return $"Transient: {_transient.GetHashCode()}, Scoped:: {_scoped.GetHashCode()}, Singleton: {_singleton.GetHashCode()}";
    }
}

接著在 Controllers 目錄下建一個新的 Controller DemoController.cs,在 constructor 中一樣注入三種 interface 的實體,再加上 DemoService 的實體;另外在 Get() 方法中,先取得其 DemoService 實體中所擁有的三個物件的 hash codes,再取得 DemoController 自己擁有的三個物件的 hash codes

# Controllers/DemoController.cs
using Microsoft.AspNetCore.Mvc;
using LifeCycleDemo.Services;
using LifeCycleDemo.Interface;
namespace LifeCycleDemo.Controllers;
[ApiController]
[Route("[controller]")]
public class DemoController : ControllerBase
{
    private readonly ITransient _transient;
    private readonly IScoped _scoped;
    private readonly ISingleton _singleton;
    public readonly DemoService _service;
    public DemoController(ITransient transient, IScoped scoped, ISingleton singleton, DemoService service)
    {
        _transient = transient;
        _scoped = scoped;
        _singleton = singleton;
        _service = service;
    }
    [HttpGet]
    public ActionResult<IDictionary<string, string>> Get()
    {
        var serviceHash = _service.GetServiceHashCode();
        var controllerHash = $"Transient: {_transient.GetHashCode()}, Scoped:: {_scoped.GetHashCode()}, Singleton: {_singleton.GetHashCode()}";
        return new Dictionary<string, string> {
            {"(First call) Obj Hash Codes in DemoService", serviceHash},
            {"(Second call) Obj Hash Codes in DemoController", controllerHash}
        };
    }
}

最後在 Program.cs 中加入 DemoClass (三種不同生命週期)及 DemoService

# Program.cs
using LifeCycleDemo.Interface;
using LifeCycleDemo.Services;
...
// Add services to the container.
builder.Services.AddTransient<ITransient, DemoClass>();
builder.Services.AddScoped<IScoped, DemoClass>(); 
builder.Services.AddSingleton<ISingleton, DemoClass>();
builder.Services.AddScoped<DemoService, DemoService>();

準備完畢!到專案目錄下使用 dotnet CLI 啟動網站

C:\Users\Jim\ObjLifeCycle>dotnet run
Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: <https://localhost:7197>
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: <http://localhost:5111>
...

連上 https://localhost:7197/demo,會看到如下畫面:

這裡發生的事情是:在初始化 DemoController 的時候,首先創造了三個 DemoClass 的實體 (分別為_transient, _scoped, _singleton 型別),接著創造 DemoService 實體,DemoService 自己在初始化過程中也會創造三個不同型別的 DemoClass 實體。這些實體到底是不是同一個?例如:DemoClass 的 _singleton 與 DemoService 的 _singleton 是同一個嗎?從物件的 hash code 就能判斷:

  • _singleton 物件只會創建一次並且永遠共享,相同的 hash code 表示物件是同一個,且只有這一個
  • _transient 物件每次都會創建新的,所以 hash code 永遠不同
  • _scoped 物件是以請求 (request) 為分界,在同一次對於 DemoController 的 Get 請求中,_scoped 物件是共用同一個 (hash code 相同)

再發送一次請求可以看得更清楚。不要關閉分頁,再另開一個分頁,一樣連上 https://localhost:7197/demo,會看到如下畫面:

  • _singleton 物件還是同一個 (hash code: 59481678 與上一個請求相同)
  • _transient 物件永遠都不一樣
  • 在這次請求中的 _scoped 物件是同一個 (hash code: 16839133),但與上一個請求的物件不同 (hash code: 9093001)

使用時機

該如何決定物件/服務的生命週期?

  • 如果你不確定該使用那個,Scoped 通常是最佳選擇,在同一個請求內使用(共享)相同物件,可節省運算資源 ;不同請求使用不同物件,可避免 concurrency issue。DbContext 是一個使用 Scoped 的好例子
  • Singleton 適合整個 App 共享的資源,如 logger, cache 等
  • Transient 可能消耗最多運算資源,但對於避免 multithreading issue 也是最保險的作法

附上其他說明供參考