[C#]MS.DIにおけるサービスのライフタイムとスコープの関係について

Microsoft.Extensions.DependencyInjection(以下、MS.DI)を使用すると、依存性注入によりサービスのライフタイムを制御できます。ライフタイムには主に以下の3つがあります。

  • Singleton
  • Transient
  • Scoped

今回は、これらのライフタイムがスコープの設定によってサービスのインスタンス(Idの値)にどのような影響を与えるかを、単体テストを通じて確認します。

単体テストのコード

以下の単体テストコードでは、ISomeServiceというインターフェースと、それを実装するSomeServiceクラスを使用しています。SomeServiceは、インスタンスごとに異なるIdGuid)を持ちます。

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:
    • 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では別のインスタンスが生成される。

サービスのライフタイムとスコープの理解は、正しい依存性注入の実装に不可欠です。特に、サービスが状態を持つ場合やリソースの消費を最適化したい場合、適切なライフタイムを選択することでアプリケーションのパフォーマンスと信頼性を向上させることができます。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です