Microsoft.Extensions.DependencyInjection(以下、MS.DI)を使用すると、依存性注入によりサービスのライフタイムを制御できます。ライフタイムには主に以下の3つがあります。
- Singleton
- Transient
- Scoped
今回は、これらのライフタイムがスコープの設定によってサービスのインスタンス(Id
の値)にどのような影響を与えるかを、単体テストを通じて確認します。
単体テストのコード
以下の単体テストコードでは、ISomeService
というインターフェースと、それを実装するSomeService
クラスを使用しています。SomeService
は、インスタンスごとに異なるId
(Guid
)を持ちます。
public interface ISomeService { Guid Id { get; } } public class SomeService : ISomeService { public Guid Id { get; } = Guid.NewGuid(); }
Singletonの場合
Test1: スコープを設定しない場合
[Fact] public void Test1() { var services = new ServiceCollection(); services.AddSingleton<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var service = provider.GetService<ISomeService>(); var service2 = provider.GetService<ISomeService>(); Assert.NotNull(service); Assert.NotNull(service2); Assert.Equal(service.Id, service2.Id); }
Test1_1: スコープを設定する場合
[Fact] public void Test1_1() { var services = new ServiceCollection(); services.AddSingleton<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var scopeFactory = provider.GetService<IServiceScopeFactory>(); Assert.NotNull(scopeFactory); using var scope1 = scopeFactory.CreateScope(); var service = scope1.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service); using var scope2 = scopeFactory.CreateScope(); var service2 = scope2.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service2); Assert.Equal(service.Id, service2.Id); }
Transientの場合
Test2: スコープを設定しない場合
[Fact] public void Test2() { var services = new ServiceCollection(); services.AddTransient<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var service = provider.GetService<ISomeService>(); var service2 = provider.GetService<ISomeService>(); Assert.NotNull(service); Assert.NotNull(service2); Assert.NotEqual(service.Id, service2.Id); }
Test2_1: スコープを設定する場合
[Fact] public void Test2_1() { var services = new ServiceCollection(); services.AddTransient<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var scopeFactory = provider.GetService<IServiceScopeFactory>(); Assert.NotNull(scopeFactory); using var scope1 = scopeFactory.CreateScope(); var service = scope1.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service); using var scope2 = scopeFactory.CreateScope(); var service2 = scope2.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service2); Assert.NotEqual(service.Id, service2.Id); }
Scopedの場合
Test3: スコープを設定しない場合
[Fact] public void Test3() { var services = new ServiceCollection(); services.AddScoped<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var service = provider.GetService<ISomeService>(); var service2 = provider.GetService<ISomeService>(); Assert.NotNull(service); Assert.NotNull(service2); Assert.Equal(service.Id, service2.Id); }
Test3_1: スコープを設定する場合
[Fact] public void Test3_1() { var services = new ServiceCollection(); services.AddScoped<ISomeService, SomeService>(); var provider = services.BuildServiceProvider(); var scopeFactory = provider.GetService<IServiceScopeFactory>(); Assert.NotNull(scopeFactory); using var scope1 = scopeFactory.CreateScope(); var service = scope1.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service); using var scope2 = scopeFactory.CreateScope(); var service2 = scope2.ServiceProvider.GetService<ISomeService>(); Assert.NotNull(service2); Assert.NotEqual(service.Id, service2.Id); }
結果のまとめ
AddSingletonの場合
- スコープを設定しない場合でも、設定した場合でも、
Id
は変わらない。 - 同一のインスタンスが常に提供される。
AddTransientの場合
- スコープを設定しない場合でも、設定した場合でも、
Id
は毎回変わる。 - 新しいインスタンスが常に生成される。
AddScopedの場合
- スコープを設定しない場合、
Id
は変わらない。- デフォルトでは、
BuildServiceProvider
で生成されたルートスコープが使用されるため。
- デフォルトでは、
- スコープを設定した場合、スコープ間で
Id
が変わる。- 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では新しいインスタンスが生成される。
詳細な解説
1. Singletonの動作
AddSingleton
で登録されたサービスは、アプリケーション全体で単一のインスタンスが使用されます。スコープを設定しても、同じインスタンスが返されるため、Id
は変わりません。
- ポイント: スコープに関係なく、常に同じインスタンスが提供される。
2. Transientの動作
AddTransient
で登録されたサービスは、サービスを取得するたびに新しいインスタンスが生成されます。スコープの有無にかかわらず、毎回異なるId
となります。
- ポイント: スコープに関係なく、常に新しいインスタンスが生成される。
3. Scopedの動作
AddScoped
で登録されたサービスは、スコープごとに一つのインスタンスが提供されます。
- スコープを設定しない場合:
BuildServiceProvider
で生成されたルートスコープが使用されます。- そのため、同じインスタンスが返され、
Id
は変わりません。
- スコープを設定した場合:
- 新しいスコープ(
IServiceScope
)を作成することで、そのスコープ内では同じインスタンスが使用されますが、異なるスコープ間では別のインスタンスが生成されます。 - したがって、
Id
はスコープごとに異なります。
- 新しいスコープ(
- ポイント: 同一スコープ内では同じインスタンス、異なるスコープ間では異なるインスタンスが提供される。
スコープの作成と管理
スコープは通常、WebアプリケーションではHTTPリクエストごとに自動的に作成されます。しかし、コンソールアプリケーションや単体テストでは、以下のように明示的にスコープを作成する必要があります。
var scopeFactory = provider.GetService<IServiceScopeFactory>(); using var scope = scopeFactory.CreateScope();
実用的なポイント
- Singletonを使う場合:
- アプリケーション全体で共有する設定やリソースに適しています。
- スレッドセーフである必要があります。
- Transientを使う場合:
- ステートレスなサービスや、一時的なデータを扱う場合に適しています。
- Scopedを使う場合:
- リクエストごとに状態を持たせたい場合に適しています。
- Webアプリケーションでは特に有用です。
まとめ
- AddSingleton:
- スコープに関係なく、常に同じインスタンスが提供される。
- AddTransient:
- スコープに関係なく、常に新しいインスタンスが生成される。
- AddScoped:
- 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では別のインスタンスが生成される。
サービスのライフタイムとスコープの理解は、正しい依存性注入の実装に不可欠です。特に、サービスが状態を持つ場合やリソースの消費を最適化したい場合、適切なライフタイムを選択することでアプリケーションのパフォーマンスと信頼性を向上させることができます。
コメントを残す