ASP.NET CoreにおけるDI(Dependency Injection)

ASP.NET Coreでは「DI(Dependency Injection)」を基本として使用するアーキテクチャが採用されています。
DI自体は古くからある考え方であり、Javaなどでは昔から、そして今でもメジャーに使われている技術です。 勿論 .NET 開発者の間でも利用されています。古くはSeaserコンテナから始まり、MEF(Managed Extensibility Framework)、Unity、Ninject、AutoFac・・・等いくつものDIコンテナが存在しています。

DIは基本的に「特定の具象クラスに依存した実装を行わない」ことを実現します。つまり、あるオブジェクトAがあるオブジェクトBを使用する(依存する)場合、Aの中で直接 new B() のようなことはしないのです。
以下が非DIな実装です。ShoppingCartControllerクラスは、ShoppingCartServiceクラスを明示的に指定してnewでインスタンス化しています。

// 非DIな実装
public class ShoppingCartController : Controller {
  public IActionResult AllList() {
    ShoppingCartService svc = new ShoppingCartService();
    this.ViewBag.Items = svc.GetItems(userID);
    return View();
  }
}

とはいえ、上記のような実装はよく見かける実装です。
一般的に比較的小規模なプロジェクトではDIを使用しないことが多かったことと思われます。DIは冗長な実装を必要とし、オブジェクトの生成をバックエンドに隠すため、一歩間違うと見通しが悪い(もしくはDIを理解していないプログラマにとっては分かりにくい)コードになる危険性があります。
しかしDIを使用する事のメリットは、やはり「テストしやすい」プログラム構造になるという事です。TDDやアジャイルといった開発手法を採用する場合、ユニットテストコードは非常に重要な意味を持ちます。特定のオブジェクトをテストする際には、そのオブジェクトが使用する(依存する)オブジェクトはMockに差し替え、テストしたい対象に絞ったテストコードを動作させたいものです。

ASP.NET CoreのDI

ASP.NET Coreでは「標準の組み込み機能」としてDI機能をサポートしています。そして、ASP.NET Core開発ではDIを使用する事がデフォルトとなるでしょう。
DIには一般的に「コンストラクタ インジェクション」「プロパティ インジェクション」「インターフェイス インジェクション」といったインジェクションの方式があります。これらのうち、ASP.NET Core組み込みのDI機能は「コンストラクタ インジェクション」のみをサポートします。

ASP.NET MVC Coreのコントローラクラスにサービスクラスをインジェクションする例を以下に示します。

インジェクションされるクラスの用意

MVCコントローラにインジェクションされるサービスクラスとして「SmileServiceクラス」を用意します。
このクラスは「Hi()」というメソッドを持ち、呼び出すと笑顔を返してくれます。SmileServiceクラスという具象クラスをインジェクションすることもできるのですが、現実的にMockへの差し替えを考慮し「ISmileServiceインターフェイス」も合わせて用意する事とします。

namespace DiExample1.Services
{
  // スマイルインターフェイス
  public interface ISmileService
  {
    string Hi();
  }

  // スマイルインターフェイスの実装クラス
  public class SmileService : ISmileService
  {
    public string Hi()
    {
      return "(^o^)";
    }
  }
}

DIサービスコンテナへの登録

DIサービスコンテナへの登録は、StartupクラスのConfigureServices()メソッドで行います。
引数として受け取った、IServiceCollectionオブジェクトに対してサービスクラスを登録します。
以下のサンプルでは「AddTransient()メソッド」によりオブジェクトをDIサービスコンテナに登録しています。AddTransient()メソッドにはいくつかのオーバーロードがありますが、以下は「ISmileService型の要求に対してはSmileServiceオブジェクトをインジェクションする」という意味になります。

using DiExample1.Services;

namespace DiExample1
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      // ISmileServiceをDIサービスコンテナに登録
      services.AddTransient<ISmileService, SmileService>();

      // Add framework services.
      services.AddMvc();
    }
    ...省略...
  }
}

MVCコントローラーでオブジェクトを受け取る

では、MVCコントローラーでISmileServiceオブジェクトを受け取ります。
前述のとおりASP.NET CoreのサポートするDIは「コンストラクタ インジェクション」です。その為、コンストラクタの引数にインジェクションして欲しいオブジェクト型の引数を宣言します。
以下のサンプルコードではISmileService型引数をMVCコントローラのコンストラクタに記述しています。「ISmileService」という型は、Startup.ConfigureServices()でDI対象オブジェクトとして登録されています。その為、ASP.NET CoreのDI機能はISmileService型オブジェクトであるSmileServiceオブジェクトをインジェクションします。
インジェクションされたサービスオブジェクトは、コントローラメソッド内で使用したい為、privateフィールドに退避しています。Index()アクションメソッド内においてSmileServiceオブジェクトのHi()メソッドを呼び出し、その結果をViewBagに格納しViewに受け渡しています。

using Microsoft.AspNetCore.Mvc;
using DiExample1.Services;

namespace DiExample1.Controllers
{
  public class HomeController : Controller
  {
    private ISmileService _smileService = null;

    // コンストラクタでインジェクションされる
    public HomeController(ISmileService smileService)
    {
      this._smileService = smileService;
    }

    public IActionResult Index()
    {
      this.ViewBag.Smile = this._smileService.Hi();

      return View();
    }
  }
}

Viewは単純に以下の実装を行いました。

<html>
  <body>
    @this.ViewBag.Smile
  </body>
</html>

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

オブジェクトのライフタイム

前述では「IServiceCollection.AddTransient()メソッド」によりインジェクション対象のオブジェクトをASP.NET Core DIサービスコンテナに登録しました。
DIオブジェクトには「ライフタイム」という概念が存在します。AddTransient()は、その名前の通り 儚く インジェクションされる度に生成・破棄されます。
ライフタイムオプションの一覧は以下の通りです。

ライフタイム 登録メソッド 説明
Transient AddTransient() インジェクション毎にインスタンスが生成されます。
Scoped AddScoped() 1リクエスト毎に1インスタンス生成されます。
Singleton AddSingleton() アプリケーション内で1つのインスタンスが生成されます。

Scopedは、1リクエスト内で行われる処理における、複数のサービスクラスやドメインクラスでの処理を跨ってインスタンスが維持される必要がある場合に使用します。
Singletonは、アプリケーション動作内で1つのインスタンスを共有したい場合に使用します。
先程のサンプル実装にSingletonオブジェクトのDIを追加したサンプルを以下に示します。

Singletonサービスオブジェクトを用意する

ISingletonSmileServiceインターフェイス、その実装であるSingletonSmileServiceクラスを用意します。
Singletonである特性を生かすために、Hi()メソッドが呼び出されるごとにSmileした回数をカウントする仕組みとします。

namespace DiExample1.Services
{
  public interface ISingletonSmileService
  {
    string Hi();
  }

  public class SingletonSmileService : ISingletonSmileService
  {
    private int _count = 0;

    public string Hi()
    {
      this._count++;
      return string.Format("(^o^) {0}笑い", this._count.ToString());
    }
  }
}

DIサービスコンテナにシングルトン登録する

Startup.ConfigureServices()メソッド内でAddTransient()に続きAddSingleton()を呼び出します。

using DiExample1.Services;

namespace DiExample1
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      // ISmileServiceをDIへ登録
      services.AddTransient<ISmileService, SmileService>();
      services.AddSingleton<ISingletonSmileService, SingletonSmileService>();

      // Add framework services.
      services.AddMvc();
    }
    ...省略...
  }
}

MVCコントローラでシングルトンオブジェクトを受け取る

Transientオブジェクトと同様にコンストラクタ引数でインジェクション対象のオブジェクトを受け取ることができます。
以下のコードからわかるように、コンストラクタ引数を複数宣言することで、複数のオブジェクトがインジェクションされます。

using Microsoft.AspNetCore.Mvc;
using DiExample1.Services;

namespace DiExample1.Controllers
{
  public class HomeController : Controller
  {
    private ISmileService _smileService = null;
    private ISingletonSmileService _singletonSmileService = null;

    public HomeController(
      ISmileService smileService,
      ISingletonSmileService singletonSmileService)
    {
      this._smileService = smileService;
      this._singletonSmileService = singletonSmileService;
    }

    public IActionResult Index()
    {
      this.ViewBag.Smile = this._smileService.Hi();
      this.ViewBag.SingletonSmile = this._singletonSmileService.Hi();

      return View();
    }
  }
}

Viewはこんな感じ。

<html>
  <body>
    @this.ViewBag.Smile
    <br />
    @this.ViewBag.SingletonSmile
  </body>
</html>

以下の画像のように、リクエスト毎にカウントアップされていきます。つまり、アプリケーション単位で(リクエストを跨って)単一のSingletonSmileServiceオブジェクトがインジェクションされている事が確認できます。

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

F5クリック

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

構成ファイルによるインジェクション定義には未対応

DIコンテナにはハードコーディングによるインジェクションオブジェクト定義だけではなく、JSONXMLファイルによるインジェクション構成定義をサポートするものがあります。
ASP.NET Core DIでは、このような機能はサポートしていません。
仮に構成によるMockと実クラスの切り分けを行う場合、以下のような実装も一例として考えられるでしょう(一例であり、これがベストプラクティスではありません。以下は簡易な例であり、実プロジェクトではもう少し検討事項が出るはずです)。

DI構成ファイルを作成

injectionSettings.jsonというインターフェイス型とインジェクションオブジェクト型の定義構成ファイルを用意。

{
  "injectionSettings": {
    "Mock": {
      "ISmileService": "DiExample1.Services.SmileServiceMock"
    },
    "Production": {
      "ISmileService": "DiExample1.Services.SmileService"
    }
  }
}

sppSettings.jsonで動作モードを切り替え

sppSettings.jsonに動作モード切替用のフラグを用意します。

{
  "injectionMode":  "Production" // or "Mock"
}

DIサービスコンテナへの動的登録

StartupクラスのコンストラクタでinjectSettings.jsonを構成情報として読み込みます。
その後、ConfigureServices()メソッド内でDIサービスコンテナへの登録の際に、インターフェイス型に対する実クラスタイプを「appSettings.jsonの動作フラグ」「injectionSettings.jsonの型定義」から動的に生成します。

namespace DiExample1
{
  public class Startup
  {
    public Startup(IHostingEnvironment env)
    {
      // injectionSettings.jsonを構成として読み込み
      var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddJsonFile($"injectionSettings.json", optional: true)
        .AddEnvironmentVariables();
      Configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
      // appSettings.jsonのinjectionModeを元に実装クラスタイプを動的に取得する
      services.AddTransient(
        typeof(ISmileService),
        Type.GetType(this.Configuration.GetValue<string>(
          string.Format("injectionSettings:{0}:ISmileService",
            this.Configuration.GetValue<string>("injectionMode")))));

      // Add framework services.
      services.AddMvc();
    }
    ...省略...
}

DIサービスコンテナの差し替え

ASP.NET Core DIは基本的な機能の提供となります。世の中のDIコンテナはより高度な機能が実装されています。例えば「プロパティインジェクション」であったり「JSONXMLなどの構成ファイルによるインジェクション定義機能」であったりです。
ASP.NET DIサービスコンテナは置き換え可能な実装がなされています。Autofacなどは既にASP.NET Coreに対応しているようです。
この辺りは、本投稿では深入りせず終わりたいとは思います。機会があったら、ASP.NET CoreでのAutofacの利用なども書いてみようかな・・・と。

おまけ ~ DIとIoC

「DI(Dependency Injection)」というと、用語としては「IoC(inversion of control)」がセットで出てきます。
日本語では、DI=「依存性の注入」であり、「IoC」=「制御の反転」です。
なんだかよく分からないですよね。難しい理解できない言葉を使って、これらについて知識のない人たちを煙に巻いているのかとさえ思えてしまいます。

まずDI。依存性の注入・・・依存性を注入する・・・ここでいう依存性(Dependency)とは「(他者から利用される)オブジェクト」の事を指します。
Wikipediaでも以下のように記述されています。

A dependency is an object that can be used (a service)

「オブジェクトの注入」と考えると、分かりやすいですね。まさに前述サンプルで説明した事、そのものです。

次に「IoC(Inversion of Control)」。IoCを実現する1つの方法としてDIが存在する、というものです。
で、日本語の「制御の反転」・・・これもまた意味不明ですよね。
「あるクラスA」が「あるサービスクラスB」を利用する場合を想定します。
何も考えなければ、クラスAがクラスBを生成して(newして)使用します。

public class A {
  public A()
  {
    B b = new B();
  }
}

これに対してIoCの考えを適用した場合、「クラスBを欲するクラスA」に対して外部から「クラスBのインスタンス」を注入します。

public class A {
  public A(B b)
  {
  }
}

つまり、自ら生成するのに対して、外部から注入されるようになった、つまり制御が反転した、ということです。