アレがアレでアレ

(できれば)プログラミング関係のことを書きたい

ASP.NET Core DIのライフタイムの種類

ASP.NET CoreのDIで設定できるライフタイムについてのメモです。
DIについては特に説明しません。

インスタンスのライフタイム

docs.microsoft.com

公式ドキュメントにはService lifetimesと書かれていますが、注入するインスタンスのライフタイムは以下の3つから選択することができます。

  • Transient
  • Scoped
  • Singleton

Transient

Transientは指定したクラスのインスタンスが要求される度に毎回新しく生成します。
インスタンスはリクエストの終了時に破棄されます。

AddTransient拡張メソッドで設定します。

Scoped

Scopedリクエストごとインスタンスを生成します。
Transientと違い、1リクエスト内では同じオブジェクトを使い回すようです。

Entity FrameworkのDbContextはデフォルトでscopedとなっているみたいですね。
インスタンスが破棄されるタイミングはTransientと同じです。

AddScoped拡張メソッドで設定します。

Singleton

Singletonは最初のリクエスト時にインスタンスを生成し、それ以降はそのインスタンスをずっと使いまわします。
アプリが終了するまでインスタンスは破棄されないまま残り続けます。

AddSingleton拡張メソッドで設定します。

実際に確認してみる

それぞれのライフタイム設定の違いを確認してみます。

確認用に以下のコードを用意しました。

public interface IMyDependency
{
    Guid Guid { get; }
}

public class MyDependency : IMyDependency
{
    private readonly Guid _guid;

    public MyDependency()
    {
        _guid = Guid.NewGuid();
    }

    public Guid Guid => _guid;
}

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public Guid Guid { get; set; }

    public IndexModel(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }

    public void OnGet()
    {
        Guid = _myDependency.Guid;
    }
}

IndexModelがGuidを持つMyDependencyクラスに依存している状態です。

IndexModelMyDependencyが注入されるように設定します。
まずはSingletonから確認します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMyDependency, MyDependency>(); // new
    services.AddRazorPages();
}

この状態でアプリを起動して、IndexModelのGuidを表示してみます。

f:id:eiken7kyuu:20201228132221p:plain

上記のような文字列が出てくると思いますが、何度リロードしても同じ値が表示されているかと思います。アプリを再起動しないと値が変化しません。
ちゃんとSingletonになっていますね。

次はTransientとScopedを比較してみます。

比較しやすいようにコードを修正します。

public interface IMyService
{
    Guid Guid { get; }
}

public class MyService : IMyService
{
    private readonly IMyDependency _myDependency;

    public MyService(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }

    public Guid Guid => _myDependency.Guid;
}

public class IndexModel : PageModel
{
    private readonly IMyService _myService1;
    private readonly IMyService _myService2;

    public Guid Guid1 { get; set; }
    public Guid Guid2 { get; set; }

    public IndexModel(IMyService myService1, IMyService myService2)
    {
        _myService1 = myService1;
        _myService2 = myService2;
    }

    public void OnGet()
    {
        Guid1 = _myService1.Guid;
        Guid2 = _myService2.Guid;
    }
}

新たにMyServiceというクラスを追加し、依存関係を以下のようにしました。

IndexModel -> IMyService -> IMyDependency

この状態でMyDependencyをTransinetで指定した場合とScopedで指定した場合のGuid1, Guid2の違いを見てみます。

まずはTransinet

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyDependency, MyDependency>(); // new
    services.AddTransient<IMyService, MyService>(); // new
    services.AddRazorPages();
}

IndexModelのGuid1とGuid2はそれぞれ別の値が表示されると思います。

f:id:eiken7kyuu:20201228133453p:plain

MyDependencyにTransientを指定しているのでmyService1myService2が持つMyDependencyは別のインスタンスであると確認できました。

今度はMyDependencyにScopedを指定してみます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>(); // new
    services.AddTransient<IMyService, MyService>();
    services.AddRazorPages();
}

Guid1と2の表示結果 f:id:eiken7kyuu:20201228133918p:plain

どちらも同じ値になりました。
Scopedはリクエスト毎にインスタンスを生成し、リクエスト内で使い回すため、
myService1myService2は同じMyDependencyインスタンスを参照しているってことになりますね。ちゃんとScopedが機能していました!

ざっくりまとめ

ドキュメントだけではTransientとScopedの違いがよくわかりませんでしたが、実際に動作確認することで違いが理解できました。