この記事では、ASP.NETの依存性注入(Dependency Injection, DI)でよく使われる「addScoped、addTransient、addSingleton」についての記事です。
新しい職場がC#で初めてみたものだったので自分の頭の整理含めて記事にまとめようと思います。
違いがよく分かるように実際にコードをつかって紹介していきます。
この記事を読むメリット
- addScoped、addTransient、addSingletonの違いが理解できる
- addScoped、addTransient、addSingletonそれぞれの使い所がわかる
DI(依存性の注入)について
まずは前提として、DIについて軽く整理します。
DI(Dependency Injection)は依存性の注入とよく言われますが、言葉だけだと何のことかわかりません。
自分もきちんと理解できているかと言われれば怪しいですが、「クラスの中で別のクラスを自分で new して使うのではなく、外部からインスタンスを注入してもらう形でオブジェクトを扱う」ということだと思っています。
public class HomeController : Controller
{
private readonly IMyService _service;
public HomeController(IMyService service)
{
_service = service;
}
}
インターフェース経由でインスタンスを扱うことで色々とメリットがあるようです。
メリットは以下のようなものがあります。
- クラス間の依存関係を少なくできる
- テストがしやすい
- 再利用性・拡張性が高まる
ここで、「インターフェースに対してどのクラスをどのライフサイクルで使うか」を登録する必要があります。
この登録の際に、ライフサイクルの設定をするのが「addScoped、addTransient、addSingleton」になります。
それぞれ「インスタンスがどのタイミングで生成され、いつ破棄されるか」決めることができます。
addScoped、addTransient、addSingletonの違い
どのように使われるか整理できましたので実際にそれぞれの違いを見ていきます。
表にすると、以下のようになります。
ライフサイクル | 生成タイミング | 特徴 |
---|---|---|
addTransient | 依存注入されるたびに毎回生成される | 軽量で状態を持たない処理に最適 |
addScoped | リクエストごとに1回生成される | リクエストの状態を保持して使いたいケース向き |
addSingleton | アプリ起動時に1回だけインスタンスが生成される | アプリ全体で共有されるインスタンスに最適 |
エンジニアにおすすめ書籍
エンジニアになりたて、これから勉強を深めていきたいという方におすすめの書籍はこちら!
実際のコードで違いを確認
言葉だけだとイメージしづらいので実際のコードで確認してみたいと思います。
まずはサンプルのServiceクラスを作成します。
namespace backend.Service
{
public interface IGuidService
{
string GetGuid();
}
public class GuidService : IGuidService
{
private readonly string _guid;
public GuidService()
{
_guid = Guid.NewGuid().ToString();
Console.WriteLine($"New GuidService Created: {_guid}");
}
public string GetGuid()
{
return _guid;
}
}
}
このGuidServiceは、「Guid.NewGuid()」でGUIDを生成して、それを返すだけの単純なクラスです。
ちなみに、Guidは、「Globally Unique Identifier(グローバル一意識別子)」の略で、グローバルに一意のランダム文字列です。
続いてContoller側で先ほどのServiceを呼び出します。
using backend.Services;
using Microsoft.AspNetCore.Mvc;
namespace backend.Controllers
{
[ApiController]
[Route("api/guid")]
public class TestController : ControllerBase
{
private readonly IGuidService _service1;
private readonly IGuidService _service2;
public TestController(IGuidService service1, IGuidService service2)
{
_service1 = service1;
_service2 = service2;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
First = _service1.GetGuid(),
Second = _service2.GetGuid()
});
}
}
}
最後にDIの登録を行います。
ここの登録の仕方で挙動がどう変わるか確認するため、以下のようにしています。
builder.Services.AddTransient<IGuidService, GuidService>();
// builder.Services.AddScoped<IGuidService, GuidService>();
// builder.Services.AddSingleton<IGuidService, GuidService>();
addScoped、addTransient、addSingletonでそれぞれ登録するコードを書き、コメントアウトで1つずつ挙動を確認してみます。
AddTransientの挙動
まずはAddTransientの挙動を確認します。
{
"first": "bbb54ca1-12a5-4c0c-bfe1-a7b8ee5e515e",
"second": "77cee4a0-4399-4c9a-89d0-045feaf19884"
}
結果はこうなりました。
idが異なるので、別の新しいインスタンスが生成されていることがわかります。
addScopedの挙動
まずはaddScopedの挙動を確認します。
{
"first": "1d369988-e8f9-48c6-9dc4-97de05d44ad2",
"second": "1d369988-e8f9-48c6-9dc4-97de05d44ad2"
}
結果はこうなりました。
同じリクエスト内での処理になるので、同じインスタンスが使用されます。
別のリクエストで同じ処理を実行するapiを作成しました。
[HttpGet("ver2")]
public IActionResult GetVer2()
{
return Ok(new
{
First = _service1.GetGuid(),
Second = _service2.GetGuid()
});
}
これで確認すると、以下の結果が返ってきます。
{
"first": "ba535e06-1035-465e-a3a9-4e71a94ff436",
"second": "ba535e06-1035-465e-a3a9-4e71a94ff436"
}
リクエストが違うと違うインスタンスが使われることが分かります。
addSingletonの挙動
最後はaddSingletonの挙動を確認します。
{
"first": "d359fe1f-a318-4cba-bf0b-18a203f0ac2c",
"second": "d359fe1f-a318-4cba-bf0b-18a203f0ac2c"
}
addScopedと同じように、同じインスタンスを使われています。
別リクエストでの結果は以下になります。
{
"first": "d359fe1f-a318-4cba-bf0b-18a203f0ac2c",
"second": "d359fe1f-a318-4cba-bf0b-18a203f0ac2c"
}
別リクエストでも同じインスタンスが使われています。
addScoped、addTransient、addSingletonの違いまとめ
改めて違いを整理してみると以下のようになります。
ライフサイクル | 生成タイミング | 特徴 | 同一リクエスト内 | リクエストまたぎ |
---|---|---|---|---|
addTransient | 依存注入のたびに毎回生成 | 軽量で状態を持たない処理に最適 | 別インスタンス | 別インスタンス |
addScoped | リクエストごとに生成 | リクエストの状態を保持して使いたいケース向き | 同じインスタンス | 別インスタンス |
addSingleton | アプリ起動時に1回だけ生成 | アプリ全体で共有されるインスタンスに最適 | 同じインスタンス | 同じインスタンス |
実際にコードを書いて確かめてみると違いを整理しやすかったです。
理解が曖昧だったりした場合はこのようにコードで確認してみるのがおすすめです。
自分もこの機会に整理ができてよかったです。