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:
- 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では別のインスタンスが生成される。
サービスのライフタイムとスコープの理解は、正しい依存性注入の実装に不可欠です。特に、サービスが状態を持つ場合やリソースの消費を最適化したい場合、適切なライフタイムを選択することでアプリケーションのパフォーマンスと信頼性を向上させることができます。
