この記事では、BlazorにおけるDIの各Scopeについて、Blazor Server、Blazor WebAssemblyそれぞれの観点で見てみようと思います。

本記事はQiitaのミラーです。
https://qiita.com/yoshi1220/items/4ebb2e2fc5528e16108b

DIとは?

DIとはDependency Injectionの略で、依存性注入と訳されます。クラスを疎結合にし、柔軟なソフトウェア開発を可能にします。
例えば、メール送信のためにサードパーティのライブラリがあるとします。通常、このライブラリを利用する場合は、ライブラリのクラスのインスタンスを作成して利用します。この方法ですと、ライブラリのクラスと使用するクラスが密結合になってしまいます。
DIをすると、インターフェイスを経由してクラスのインスタンスを取得できるため、クラス間を疎結合にできます。
DIの詳細については、ここでは述べませんので、他の記事や書籍等を参照してください。

DIのScopeとは?

DIによってクラスのインスタンスを柔軟に生成できますが、インスタンスを生成する際に、インタンスが有効な範囲を決められます。このインスタンスが有効な範囲がScopeになります。ASP.NET Core Blazorの標準のDIを利用する場合、Transient、Scoped、Singletonの3つの範囲でインスタンスを管理できます。それぞれ基本的な範囲は以下のようになりますが、Blazor ServerかBlazor WebAssemblyかによって動作に違いがあります。

範囲 説明
Transient コンポーネントにアクセスされるたびにインスタンスが生成されます。
Scoped 利用ユーザー単位でインスタンスが生成されます。各コンポーネント間で共通のインスタンスが利用されます。
Singleton アプリケーション全体でインスタンスが生成されます。そのため、異なるユーザー間でもインスタンスが共有されます。

Transientの動作については、Blazor Server、Blazor WebAssemblyの違いはありませんが、Scoped、Singletonについては動作に違いがあるため、それぞれ解説していきたいと思います。

Blazor Serverにおける各Scopeの動作について

Blazor Serverにおける各Scopeの動きを確認したいと思います。まずは、標準で生成されるCounterコンポーネントのカウント数を保持するサービスを作成します。

public class CounterService
{
    public int Value { get; set; }

    public void Increment()
    {
        Value++;
    }
}

Transientの場合

Program.csにて作成したCounterSerive.csをDIコンテナに登録します。
(.NET5までの場合は、Startup.csのpublic void ConfigureServices(IServiceCollection services)で登録してください。)

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddTransient<CounterService>(); //追加

var app = builder.Build();

Counter.razorを修正します。

@page "/counter"
@inject CounterService CounterService

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @CounterService.Value</p>

<button class="btn btn-primary" @onclick="CounterService.Increment">Click me</button>

@code {
}

動作を確認してみます。コンポーネントの表示を切り替えると、インスタンスが初期化されるため、カウントが0に戻ります。

Server_Transient.gif

Scopedの場合

今度は、CounterSerive.csをScopedでDIコンテナに登録します。

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddScoped<CounterService>(); //追加

var app = builder.Build();

コンポーネントを切り替えても、Counterのカウントが保持されています。ただし、リロードするとカウントが初期化されてしまいます。

Server_Scoped.gif

Singletonの場合

最後に、CounterSerive.csをSingletonでDIコンテナに登録します。

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddSingleton<CounterService>(); //追加

var app = builder.Build();

コンポーネントを切り替えても、Counterのカウントが保持され、別のブラウザー(別のユーザー)で表示しても同じカウントが表示されます。リロードした場合も0に戻らず、最新のカウント数が表示されます。

Server_Singleton.gif

Blazor ServerにおけるDIのScopeのまとめ

Blazor Serverは名前の通り、Server側でプロセスが実行されているため、Sigletonの場合は同一プロセス内でインスタンスが共有されます。そのため、複数のブラウザーや複数のユーザーでアクセスした場合に、同じインスタンスが使用されます。Scopedの場合は、クライアントとサーバー間のSignalR接続ごとにインスタンスが作成されます。そのため、接続が有効な限り同一のインスタンスが使用されます。ブラウザをリロードすると、再接続されるため、新しくインスタンスが生成されます。
図にしたものが以下のものになります。

image.png

Blazor WebAssemblyにおける各Scopeの動作について

続いて、Blazor WebAssemblyにおける各Scopeの動きを確認したいと思います。同様にCounterコンポーネントのカウント数を保持するサービスを使用します。

Transientの場合

動作を確認してみます。Blazor Serverの場合と同様に、コンポーネントの表示を切り替えると、インスタンスが初期化されるため、カウントが0に戻ります。

WASM_Transient.gif

Scoped、Singletonの場合

Blazor WebAssemblyの場合は、ScopedとSingletonの動作は同じ動作になります。これは、ブラウザのタブごとにWebAssemblyのプロセスが実行されるため、Singletonのプロセスが有効な範囲とScopedの範囲が同じ範囲になるためです。そのため、Singletonの場合でも、リロードすればインスタンスが再生成されます。タブが違えば別プロセスとなるため、別インスタンスとなります。

WASM_Scoped_Singleton.gif

Blazor WebAssemblyにおけるDIのScopeのまとめ

Blazor WebAssemblyでは、各タブは固有のプロセスとなるので、Scoped、Singletonでも同様の動きとなります。
ブラウザの各タブはそれぞれ独立したアプリケーションプロセスであり、共通の状態を共有しません。
図にしたものが以下のものになります。

image.png

まとめ

Blazor Server、Blazor WebAssemblyそれぞれにおいて、プロセスの実行環境が異なります。それぞれの環境の特徴を理解し、効果的にDIを活用できればと思います。