タグ: MS.DI

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

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