BlazorでSPAするぞ!(7) - DI(Dependency Injection) -正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

今回はDI(Dependency Injection)について。
まあ、今どきのフレームワークなので(そして.NET Coreの流れをくむBlazorなので)DIは標準でサポートしています。

DIって当たり前に使う時代になりすぎてて、改めてwikiを見てみました。

依存性の注入 - Wikipedia

Dependency injection - Wikipedia

Inversion of control - Wikipedia

DIという言葉を最初に提唱したのマーティン・ファウラー氏だったかぁ。。
IoCの起源は1988年かぁ。。。
とか、おっさんエンジニアは歴史と共にDI/IoCに思いをめぐらせますね。。。
ということで本題。

1. DI(Dependency Injection)

DI(Dependency Injection)は「依存性の注入」ですね。
「依存性の注入って言われても意味わからん」が、DIがメジャーになった2000年初期頃からの定番な話ですが。。。

オブジェクトの具象クラスに対してプログラミング(操作)するのではなく、もっと抽象的なインターフェイスに対して操作を行おう。
「new オブジェクト() 」とかしないで、インターフェイスを実装したオブジェクトの生成をDIフレームワークにやってもらおう。
プログラムからは具象オブジェクトは意識しない。

的なものですね。

DIによるメリットの代表は、以下のようなものでしょうか。

  • オブジェクト間の依存性・結合度が低くなる
  • テストしやすい(モックに差し替えやすい)
  • (上記にも重なるが)振る舞いの差し替えが容易

ここではDI自体の説明には注力しないので、このへんで。

という事で Blazor におけるDIの利用方法の説明に移ります。

まず、サンプルプロジェクトを作成しておきます。

dotnet new blazorwasm -o di_blazor

以下のような実装を行うこととします。

f:id:daigo-knowlbo:20190502212440p:plain

消費税計算を行うサンプルです。
税抜価格をテキストボックスに入力し、「税込価格計算」ボタンをクリックすると、税込価格が計算され画面に表示されます。
消費税計算を行う処理をDIでインジェクトするクラスとします。

2. サービスを作成

BlazorのDIはシンプルで全体の流れとしては以下のようになります。

  • 初期化時にInterfaceおよび対応する具象クラスをDIに登録する
  • オブジェクトを使いたい箇所で、Blazorフレームワークによってインターフェイスオブジェクトとしてインジェクトしてもらう

まずはDIでインジェクトしてもらう対象のサービスクラスを用意します。
消費税計算サービスクラスを作成します。そしてこのオブジェクトを、インジェクトの対象とします。
税抜価格をインプットとして、税込価格をアウトプットしてくれるクラスを作成します。また、2019/10/1以降であれば税率10%を適用します。

2.1. インターフェイス実装

以下はIConsumptionTaxCalculatorインターフェイス実装。

# Services/IConsumptionTaxCalculator.cs #

namespace di_blazor.Services {
  public interface IConsumptionTaxCalculator {
    int CalcTotalPrice(int price);
  }
}

2.2. 具象クラス実装

以下がConsumptionTaxCalculator具象クラス実装。

# Services/ConsumptionTaxCalculator.cs #

using System;

namespace di_blazor.Services {
  public class ConsumptionTaxCalculator : IConsumptionTaxCalculator {
    public int CalcTotalPrice(int price) {
      double rate = 0;
      if( DateTime.Today >= new DateTime(2019, 10, 1) )
        rate = 1.1;
      else
        rate = 1.08;
      return (int)(price * rate);
    }
  }
}

3. サービスを登録

「IConsumptionTaxCalculator / ConsumptionTaxCalculator」をBlazorのDIに登録します。
ProgramクラスのMain()メソッド でオブジェクトを登録します。
以下のように「services.AddSingleton<T, O>()」で、インターフェイスとオブジェクトの組み合わせでDI対象のオブジェクトを登録します。

# Program.cs #

...省略...
using di_blazor.Services;

namespace di_blazor
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder.CreateDefault(args);
      
      builder.Services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>();
      
      ...省略
    }
  }
}

3.1. DIオブジェクトのライフサイクル

上記サンプルでは「AddSingleton()」メソッドを使用しましたが、他に「AddTransient()」メソッドがあります。
名称から大体想像が付きますが、インジェクトされるオブジェクトの生存期間の違いがあります。
それぞれのライフサイクルは以下の通りです。

ASP.NET CoreのDI存在する「Scoped」は Blazor WebAssembly では未対応です。

サービスクラスの持つフィールド・プロパティの状態管理の性格に応じて Singleton / Transient を使い分けます。

4. コンポーネントにサービスをインジェクト

4.1. 【方法1】.razorにインジェクトする

Pages/Index.razorを改造して、ここに IConsumptionTaxCalculator をインジェクトします。

# Pages/Index.razor #

@page "/"
@using di_blazor.Services
@inject IConsumptionTaxCalculator calculator

税抜価格:<input type="text" bind="@Price" />
<br />
<input type="button" @onclick="Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>
@code {
  private int Price { get; set; }
  private int TaxedPrice { get; set; }

  public void Calc() {
    this.TaxedPrice = calculator.CalcTotalPrice(this.Price);
  }
}

【@inject】

「@inject」ディレクティブを利用してオブジェクトをインジェクトします。
以下の書式になります。

@inject 【インジェクトするオブジェクトの型】 【インジェクトする変数名】

@using により IConsumptionTaxCalculator を実装した名前空間「di_blazor.Services」を参照している事にも注意してください。

【コードブロック】

@code コードブロックでは、calculator としてインジェクトされたオブジェクトを利用することができます。
ちなみに、このコードをコンパイルしたILを、逆コンパイルすると以下のようになります。

# Indexクラス #
...省略...

[Route("/")]
public class Index : ComponentBase
{
  ...省略...

  [Inject]
  private IConsumptionTaxCalculator calculator
  {
    get;
    set;
  }

  ...省略...

  public void Calc()
  {
    TaxedPrice = calculator.CalcTotalPrice(Price);
  }
}

Index.razorから生成された Indexクラス のプロパティとして「IConsumptionTaxCalculator calculator」が定義され、[Inject]属性が付与されています。
DIされるのでIndexクラス内自体にはインスタンス生成ロジックは存在しません。

4.2. 【方法2】.razorのpartialコードビハインドクラスにインジェクトする

.razorの@codeコードブロックではなく、partialとして定義したコードビハインドクラスを利用した場合の実装は以下の通りです。
@Code{ }ブロックの実装を単純にIndexクラス(Index.razor.cs)に移行するだけです。
Index.razorで定義した @inject による「calculatorプロパティ」は、partialである.cs上のIndexクラスで参照可能です。

# Index.razor #

@page "/"
@using di_blazor.Services
@inject IConsumptionTaxCalculator calculator

税抜価格:<input type="text" @bind="Price" />
<br />
<input type="button" @onclick="Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>
# Index.razor.cs #

using System;

namespace di_blazor.Pages
{
  public partial class Index
  {
    private int Price { get; set; }
    private int TaxedPrice { get; set; }

    public void Calc() {
      this.TaxedPrice = calculator.CalcTotalPrice(this.Price);
    }
  }
}

4.3. 【方法3】.razorのComponentBase派生コードビハインドクラスにインジェクトする

次に、ComponentBase派生クラスとして実装したコンポーネントクラスにインジェクトする方法の説明になります。

ComponentBaseクラスを継承した IndexBase クラスを定義します。
Index.razorは IndexBaseクラス を継承(@inherits)する方式での実装になります。
Index.razorの定義は以下の通り。@injectはなくなり、@inherits定義を追加しています。

# Pages/Index.razor #

@page "/"
@inherits IndexBase

税抜価格:<input type="text" @bind="Price" />
<br />
<input type="button" @onclick="Calc" value="税込価格計算" />
<br />↓↓↓↓<br />
<div>税込価格:@TaxedPrice 円</div>

次にIndexBase.cs定義です。
IndexBaseクラスのプロパティ定義として明示的に [Inject]属性 を付与した「calculatorプロパティ」を定義します。
プロパティに[Inject]属性を付けてあげると、いわゆる プロパティ インジェクション によりオブジェクトが割り当てられます。

# Pages/IndexBase.cs(コードビハインドクラス) #

using System;
using Microsoft.AspNetCore.Components;
using di_blazor.Services;

namespace di_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    [Inject]
    private IConsumptionTaxCalculator calculator { get; set; }

    protected int Price { get; set; }
    
    protected int TaxedPrice { get; set; }

    public void Calc() {
      this.TaxedPrice = calculator.CalcTotalPrice(this.Price);
    }
  }
}

5. サービスクラスへのインジェクション

では、話を少し別のケースに進めます。
上記までで、.razor(もしくはコードビハインドクラス)へのDIは、@injectディレクティブで定義することができ、裏側ではプロパティ インジェクションが動作することが分かりました。
本投稿シリーズレベルのサンプル実装では、.razorのみ もしくは (ほぼエンティティな)モデルクラス程度しか利用していませんでした。ただし、もっと大きめのアプリケーションをBlazorで作成する場合、適切な責務を持ったモデルクラスやサービスクラスのようなクラス設計・実装が必要になります。
そのようなクラス(つまり .razor 以外)でDIを利用する方法について見てみましょう。

結論としては、これは ASP.NET CoreのDIと同じです。

サンプルでは、IConsumptionTaxCalculator / ConsumptionTaxCalculatorクラスがDateTimeに依存しています。
つまりシステム日時に依存した動作しかできない。
という事で、IDateTimeProvider / DateTimeProviderクラスを用意し、ConsumptionTaxCalculatorにはコレをインジェクトします。

5.1. IDateTimeProvider / DateTimeProviderクラスを作成

Servicesフォルダ配下に実装します。

# Services/IDateTimeProvider.cs #

using System;

namespace di_blazor.Services {
  public interface IDateTimeProvider {
    DateTime Today();
  }
}
# Services/DateTimeProvider.cs #

using System;

namespace di_blazor.Services {
  public class DateTimeProvider : IDateTimeProvider {
    public DateTime Today() {
      return DateTime.Today;
    }
  }
}

上記に加え、IDateTimeProviderの実装を増やすことで、DateTimeに依存しない Mockクラス を用意する事ができます(本投稿ではそこまでの実装は行わずに進めます)。

5.2. IDateTimeProvider / DateTimeProviderを登録

Mainクラスに、IDateTimeProvider / DateTimeProviderを登録します。
これは先程のIConsumptionTaxCalculator / ConsumptionTaxCalculatorと同様です。

# Program.cs #

...省略...
using di_blazor.Services;

namespace di_blazor
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder.CreateDefault(args);
      
      builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
      builder.Services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>();
      
      ...省略
    }
  }
}

5.3. ConsumptionTaxCalculatorに、IDateTimeProviderをインジェクト

ConsumptionTaxCalculatorクラスにIDateTimeProviderオブジェクトをインジェクトします。
.razorではなく、サービスクラス(コンポーネントクラス以外)にインジェクトする際は「コンストラクタ インジェクション」の形式をとります。
以下が実装になります。

# Services/ConsumptionTaxCalculator.cs #

using System;

namespace di_blazor.Services {
  public class ConsumptionTaxCalculator : IConsumptionTaxCalculator {
    private IDateTimeProvider _DateTimeProvider = null;

    public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider) {
      this._DateTimeProvider = dateTimeProvider;
    }

    public int CalcTotalPrice(int price) {
      double rate = 0;
      if( this._DateTimeProvider.Today() >= new DateTime(2019, 10, 1) )
        rate = 1.1;
      else
        rate = 1.08;
      return (int)(price * rate);
    }
  }
}

【コンストラクタ引数】

「public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider)」という形で、サービスクラスのコンストラクタの引数でDI対象の「IDateTimeProvider」オブジェクトを引数として受け取っています。
AddSingleton()やAddTransient()で登録済みのインターフェイスオブジェクトは、BlazorのDIフレームワークによってインスタンス生成時に、コンストラクタ引数としてインスタンスが渡される仕組みになっています。
ここでは、別のメソッド(CalcTotalPrice(int price))が呼び出された際に利用するため、自らのフィールド変数にオブジェクトを代入して利用しています。

6. デフォルトサービスとカスタムサービス

BalzorのDIの使い方は、すでに説明した方法になります。

インジェクトするオブジェクトには、「デフォルトサービス」と「カスタムサービス」という2つの種類があります。

すでに説明した ConsumptionTaxCalculator や DateTimeProvider をインジェクトするのは カスタムサービス という分類になります。
これとは別に「デフォルトサービス」というものがあります。
名前の通りデフォルトで用意されたサービスになり、これらをインジェクトすることができます。
以下の3つがデフォルトサービスとして用意されています。
以下の3つがデフォルトサービスとして用意されており、これらは明示的な登録(AddSingleton() / AddTransient())を行うことなく .razorコンポーネントやサービスクラスにインジェクトすることができます。

  • HttpClient
    Singletonで提供されます。
    Http通信を行うオブジェクトです。BlazorアプリからサーバのWebAPIを呼び出し時などに利用します。
  • IJSRuntime
    Singletonで提供されます。JavaScriptとの相互運用に利用します(Blazor C#からのJavaScript呼び出し等)。
  • IUriHelper
    Singletonで提供されます。URI/Nabigation状態管理ヘルパー。

「HttpClient」に関しては dotnet new blazorwasm コマンドで作成したデフォルトプロジェクトの実装において、以下のようにInjectが初期実装されています。

// Program.csのMain()内に以下の実装がデフォルトで追加されています。

  builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

ここら辺を使ったサンプルは、後々 別途ブログにあげようと思います。

7. まとめ

「BlazorでSPAするぞ!」という事で、いくつかの投稿で Blazorの基本 を歩いてきました。
まだPreview版という事で今後、仕様的な変更、改善が行われるのだと思いますが、リリースに備えて自分自身で基本を理解・整理することができたと思います。
「BlazorでSPAするぞ!」というシリーズ投稿としては、これで完結し、今後の投稿ではTips的にBlazor関連情報を書いていこうかと思います。

では、Let's enjoy Blazor together!!!