February 2016

Volume 31 Number 2

データ ポイント - ASP.NET 5/EF6 プロジェクトのリファクタリングと依存関係の挿入

James McCaffrey

本号発行直前に、マイクロソフトは ASP.NET 5 と関連スタックの名称変更を発表しました。ASP.NET 5 は ASP.NET Core 1.0 に名前が変わります。Entity Framework (EF) 7 は Entity Framework (EF) Core 1.0 に変わります。ASP.NET 5 と EF7 のパッケージと名前空間は変わりますが、それ以外はこの新しい名称が本コラムのテーマに影響することはありません。

Julie Lerman依存関係の挿入 (DI) とは、疎結合に他なりません (bit.ly/1TZWVtW、英語)。依存するクラスを別のクラスにハードコーディングするのではなく、別の場所 (理想的にはクラスのコンストラクター) からそのクラスを要求します。これは、明示的依存関係の原則 (Explicit Dependencies Principle) に従い、必要な協力関係に関する情報をクラスのユーザーに明確に伝えることが目的です。また、クラスのオブジェクト インスタンスの構成を変更するようなシナリオでは、ソフトウェアに柔軟性をもたせることができ、このようなクラスの自動テストを作成する場合も実に便利です。Entity Framework コードを中心とする環境では、リポジトリやコントローラーを作成する場合など、通常、疎結合を利用しないで、DbContext のインスタンスを直接作成します。こうした例を挙げれば枚挙にいとまもありません。実は、DI を習得して、「EF6、EF7、および ASP.NET 5 の混乱状態」(msdn.com/magazine/dn973011) のコラムに記載したコードに DI を当てはめるのが今回の目標です。たとえば、以下は DbContext のインスタンスを直接作成するメソッドです。

public List<Ninja> GetAllNinjas() {
  using (var context=new NinjaContext())
  {
    return context.Ninjas.ToList();
  }
}

このメソッドを ASP.NET 5 ソリューションで使用していること、および ASP.NET 5 にはたくさんの DI サポートが組み込まれていることから、「この例は DI サポートを利用して改善できるのではないか」という提案が EF チームの Rowan Miller からありました。問題の別の側面に集中していたため、この提案については考えたこともありませんでした。そこで、提案にしたがって、フローが機能するようになるまで、このサンプルを少しずつリファクタリングすることにしました。Miller は、Paweł Grudzień がブログ記事「Entity Framework 6 と ASP.NET 5」(bit.ly/1k4Tt4Y、英語) で作成しているすばらしい例を参考にするようアドバイスをくれましたが、そのアドバイスは採用せず、ブログのコードを単純にコピーして貼り付けることはしませんでした。代わりに、フローをしっかりと理解するために、自身の考えで取り組むことにしました。結果的には、今回作成したソリューションとブログの内容に大きな違いがないことがわかって満足しています。

制御の反転 (IoC: Inversion of Control)) と IoC コンテナーは、いつも少しやっかいだと感じるパターンです。30 年以上のコーディング経験があっても、このパターンには心が惹かれませんでした。経験豊富な開発者でも同じように感じる方はいるでしょう。この分野の著名な専門家 Martin Fowler は、IoC には複数の意味があるが、DI (IoC の考え方を明確にするために彼が作成した用語) と合わせて考えると、特定のオブジェクトの作成の主導権を握っているのがアプリケーションのどの部分かということになると指摘しています。IoC がなければ、この問題は必ず課題に直面します。

Steve Smith (deviq.com、英語) と私が共同執筆した Pluralsight コース「ドメイン駆動設計の基本原理」(bit.ly/PS-DDD、英語) では、2005 年の誕生以来 .NET 開発者の間で最も人気のある IoC コンテナーとなった StructureMap ライブラリを使用することになり、後塵を拝するかたちになりました。Smith のアドバイスもあり、IoC コンテナーのしくみやメリットは理解できましたが、あまりしっくりこないと感じていました。そこで、Miller からアドバイスを得た後、以前のサンプルをリファクタリングし、オブジェクトのインスタンスを必要とするロジックに、そのインスタンスを簡単に挿入できるようにするコンテナーを利用することにしました。

基礎固め

前述の GetAllNinjas クラスを収めたクラスの最初の問題は、using コードを繰り返し記述していることです。

using(var context=new NinjaContext)

他にも以下のようなメソッドもあります。

public Ninja GetOneNinja(int id) {
  using (var context=new NinjaContext())
  {
    return context.Ninjas.Find(id);
  }
}

DRY (同じことを繰り返さない、Don't Repeat Yourself) の原則から、潜在的に問題があることがわかります。そこで、NinjaContext インスタンスの作成をコンストラクターに移動し、_context などの変数を使って、そのコンテキストをさまざまなメソッドと共有することにします。

NinjaContext _context;
public NinjaRepository() {
  _context = new NinjaContext();
}

ただし、データの取得だけに専念すべきこのクラスで、このコンテキストの作成方法や作成タイミングを決定しなければなりません。そこで、コンテキストの作成方法や作成タイミングの決定を上流に移し、リポジトリでは挿入後のコンテキストを使用するだけにします。そのためには、別の場所で作成したコンテキストを渡すように、再度リファクタリングします。

NinjaContext _context;
public NinjaRepository(NinjaContext context) {
  _context = context;
}

これで、リポジトリはデータの取得だけになります。コードのあちこちでコンテキストを作成する必要はありません。リポジトリでは、コンテキストの構成方法、作成や破棄のタイミングを意識しません。これでこのクラスは、EF のコンテキストを管理する責任も、データベース要求を行う責任も負わなくなるため、単一責任の原則というオブジェクト指向の原理に従うことになります。リポジトリ クラスに取り組むときは、クエリに専念できます。こうした決定をテスト内で行えるようになり、自動テストでの使用方法とリポジトリでの使用方法の設計が一致しないことによってテストに支障をきたすことがないので、テストも容易になります。

前回のサンプルにはもう 1 つ問題があり、接続文字列を DbContext にハードコーディングしています。このサンプル作成時は「単なるデモ」であったこと、実行中のアプリ (ASP.NET 5 アプリケーション) で接続文字列を取得して EF6 プロジェクトに渡すのは複雑だったこと、そのコラムには別のテーマがあったことなどから、その時点では妥当だと考えていました。ただし、このプロジェクトをリファクタリングすれば、IoC を利用して実行中のアプリケーションから接続文字列を渡せるようになります。詳しくはコラムの後半をお読みください。

ASP.NET 5 による NinjaContext の挿入

ですが、NinjaContext の作成はどこに移動すればよいでしょう。コントローラーはリポジトリを使用します。もちろん、コントローラーに EF を導入して、リポジトリの新しいインスタンスに渡すのは論外です。このようにすると、コードが混乱します (以下参照)。

public class NinjaController : Controller {
  NinjaRepository _repo;
  public NinjaController() {
    var context = new NinjaContext();
    _repo = new NinjaRepository(context);
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

同時に、コントローラーに EF を認識させているため、依存関係オブジェクトのインスタンス作成に関する問題が無視されています。これは先ほどリポジトリで解決したばかりの問題です。このコントローラーでは、リポジトリ クラスのインスタンスを直接作成しています。目標は、リポジトリを使用するだけで、リポジトリの作成方法、作成や破棄のタイミングを意識しないようにすることです。NinjaContext インスタンスをリポジトリに挿入したかのように、作成済みのリポジトリ インスタンスをコントローラーに挿入するのが目標です。

コントローラーのクラス内部のコードを簡潔にしたのが以下のバージョンです。

public class NinjaController : Controller {
  NinjaRepository _repo;
  public NinjaController(NinjaRepository repo) {
    _repo = repo;
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

IoC コンテナーによるオブジェクト作成の編成

StructureMap を利用しているのではなく、ASP.NET 5 に取り組んでいるため、ASP.NET 5 の DI 向け組み込みサポートを使用することになります。ASP.NET の新しいクラスの多くが、挿入されるオブジェクトを受け取るようになっているだけでなく、ASP.NET 5 はオブジェクトの配置先を調整できるサービス インフラストラクチャ (IoC コンテナー) も備えています。また、作成および挿入するオブジェクトのスコープ (作成や破棄のタイミング) も指定できます。こうした組み込みのサポートを利用すれば、着手は簡単です。

ASP.NET 5 の DI サポートを利用すれば、必要に応じて NinjaContext と NinjaRepository を挿入できるようになります。しかしその前に、EF7 には ASP.NET 5 の DI サポートに接続する組み込みのメソッドがあるため、EF7 クラスの挿入について確認しておきます。標準 ASP.NET 5 プロジェクトに含まれる startup.cs クラスに、ConfigureServices というメソッドがあります。このメソッドでは、依存関係の接続方法をアプリケーションに指示します。そのため、適切なオブジェクトを作成後、このオブジェクトを必要とするオブジェクトに挿入できます。EF7 の構成のみを残した ConfigureServices メソッドを以下に示します。

public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFramework()
          .AddSqlServer()
          .AddDbContext<NinjaContext>(options =>
            options.UseSqlServer(
            Configuration["Data:DefaultConnection:ConnectionString"]));
}

EF6 ベースのモデルを使用する前回のプロジェクトとは異なり、この構成を実行することになるプロジェクトは EF7 に依存します。ここからは、このコードで行われていることを説明します。

project.json ファイルで EntityFramework.MicrosoftSqlServer を指定しているため、このプロジェクトは関連する EF7 アセンブリをすべて参照します。参照する EF7 アセンブリのうち、EntityFramework.Core アセンブリは、IServiceCollection に対する AddEntityFramework 拡張メソッドを提供して、Entity Framework サービスを追加できるようにします。EntityFramework.MicrosoftSqlServer dll は、AddSqlServer 拡張メソッドを提供します。このメソッドは AddEntityFramework に付加して呼び出します。AddSqlServer 拡張メソッドは、SqlServer サービスを IoC コンテナーに詰め込んで、EF がデータベース プロバイダーを検索する際に使用を認識できるようにします。

AddDbContext も EF Core アセンブリに含まれています。このコードは、指定した DbContext インスタンスを (指定したオプションと共に) ASP.NET 5 の組み込みコンテナーに追加します。コンストラクターで DbContext を要求するすべてのクラス (ASP.NET 5 を構成中の場合) には、DbContext の作成時に構成済みの DbContext が提供されます。つまり、このコードにより、サービスが必要に応じてインスタンスを作成する場合の既知の型として、NinjaContext が追加されます。さらに、このコードは、NinjaContext の作成時に SqlServer 構成オプションとして、構成コード内にある文字列 (今回の場合は ASP.NET 5 appsettings.json ファイルのプロジェクト テンプレートによって作成された文字列) を使用するよう指定しています。ConfigureService はスタートアップ コードで実行されるため、アプリケーションのコードが NinjaContext を想定していても、具体的なインスタンスが指定されていないときは、指定された接続文字列を使用して、ASP.NET 5 が NinjaContext オブジェクトの新しいインスタンスを作成して渡します。

そのため、すべてが EF7 に非常に適切に組み込まれます。残念ながら、このような機能は EF6 に存在しません。とは言え、サービスのしくみの考え方がわかったので、EF6 NinjaContext をアプリケーションのサービスに追加するパターンについて理解できるようになりました。

ASP.NET 5 用にビルドされていないサービスの追加

AddEntityFramework や AddMvc のように優れた拡張機能を備えた ASP.NET 5 と連携するようにビルドされたサービスだけでなく、他の依存関係を追加することも可能です。IServicesCollection インターフェイスは、ごく一般的な Add メソッドのほかに、追加するサービスの有効期間を指定する一連のメソッド (AddScoped、AddSingleton、および AddTransient) も提供します。今回のソリューションでは AddScoped に注目します。ここでは、要求したインスタンスの有効期間のスコープを、MVC アプリケーションの各 HTTP 要求に設定します。この MVC アプリケーションで今回の EF6Model プロジェクトを使用しています。アプリケーションは、複数の要求で 1 つのインスタンスを共有しません。これは、コントローラーの各アクション内で NinjaContext を作成および破棄することによって本来実現していることをエミュレーションします。つまり、コントローラーの各アクションが 1つの要求に対応します。

挿入されたオブジェクトを必要とするクラスは 2 つありました。NinjaRepository クラスは NinjaContext を必要とし、NinjaController は NinjaRepository オブジェクトを必要とします。

まず、startup.cs の ConfigureServices メソッドに以下を追加します。

services.AddScoped<NinjaRepository>();
services.AddScoped<NinjaContext>();

これで、アプリケーションはこのような型を認識し、別の型のコンストラクターによって要求されたときに、この型のインスタンスを作成するようになります。

コントローラーのコンストラクターは、パラメーターとして渡される NinjaRepository を探します。

public NinjaController(NinjaRepository repo) {
    _repo = repo;
  }

しかし、何も渡されていないため、サービスは実行時に NinjaRepository を作成することになります。これを「コンストラクターの挿入」と呼びます。同様に、NinjaRepository が NinjaContext インスタンスを想定していて、何も渡されないときは、サービスは NinjaContext のインスタンスを作成することを認識します。

前述のように、接続文字列は DbContext から取得していました。ここでは、NinjaContext を構築する AddScoped メソッドに対して、接続文字列について指示できるようになります。今回も appsetting.json ファイルに文字列を配置します。このファイルの該当セクションを以下に示します。

"Data": {
    "DefaultConnection": {
      "NinjaConnectionString":
      "Server=(localdb)\\mssqllocaldb;Database=NinjaContext;
      Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  }

JSON は、行の折り返しをサポートしないため、JSON ファイルの "Server=" で始まる文字列は折り返さないようにします。ここでは、読みやすくするためだけに改行しています。

NinjaContext コンストラクターを変更して、接続文字列を受け取り、それを DbContext オーバーロードで使用します。このオーバーロードは接続文字列も受け取ります。

public NinjaContext(string connectionString):
    base(connectionString) { }

これで、NinjaContext を見つけた場合、appsettings.json の NinjaConnectionString で接続文字列が渡され、このオーバーロードを使用して NinjaContext を構築する必要があることを、AddScoped に指示できます。

services.AddScoped<NinjaContext>
(serviceProvider=>new NinjaContext
  (Configuration["Data:DefaultConnection:NinjaConnectionString"]));

この最後の変更により、リファクタリングで修正したソリューションは、端から端まで機能するようになります。スタートアップ ロジックは、リポジトリとコンテキストを挿入するように、アプリをセットアップします。アプリが (コンテキストを使用するリポジトリを使用する) 既定のコントローラーにルーティングされると、必要なオブジェクトが実行時に作成され、データがデータベースから取得されます。今回の ASP.NET 5 アプリケーションは、組み込みの DI を利用して、モデルの作成に EF6 を使用していた以前のアセンブリとやり取りします。

柔軟性を目的としたインターフェイス

最後にもう 1 つ、インターフェイスを利用して機能を強化します。異なるバージョンの NinjaRepository クラスや NinjaContext クラスを使用する可能性がある場合は、全体を通してインターフェイスを実装することができます。NinjaContext のバリエーションを用意するニーズは予測できないため、リポジトリのクラス用にだけ 1 つインターフェイスを作成します。

図 1 に示すように、NinjaRepository に INinjaRepository コントラクトを実装します。

図 1. インターフェイスを使用する NinjaRepository

public interface INinjaRepository
{
  List<Ninja> GetAllNinjas();
}
public class NinjaRepository : INinjaRepository
{
  NinjaContext _context;
  public NinjaRepository(NinjaContext context) {
    _context = context;
  }
  public List<Ninja> GetAllNinjas() {
    return _context.Ninjas.ToList();
  }
}

これで、ASP.NET 5 MVC アプリケーションのコントローラーは、NinjaRepository という具体的な実装の代わりに、INinjaRepository インターフェイスを使用するようになります。

public class NinjaController : Controller {
  INinjaRepository _repo;
  public NinjaController(INinjaRepository repo) {
    _repo = repo;
  }
  public IActionResult Index() {
    return View(_repo.GetAllNinjas());
  }
}

NinjaRepository 用の AddScoped メソッドを変更して、インターフェイスが必要なときには必ず適切な実装 (現時点では NinjaRepository) を使用するように ASP.NET 5 に指示します。

services.AddScoped<INinjaRepository, NinjaRepository>();

新しいバージョンがリリースされたときや、異なるアプリケーションでインターフェイスの異なる実装を使用している場合は、正しい実装を使用するように AddScoped メソッドを変更することができます。

実際にやってみて習得 (コピーや貼り付けを行わない)

Miller が親切にソリューションのリファクタリングを勧めてくれたことに感謝しています。もちろん、コラムの流れのようにスムーズにリファクタリングできたわけではありません。単に他人のソリューションをコピーすることはしなかったため、最初は間違ったこともありました。何が間違っていたかを学習し、正しいコードを考えて成功にたどり着けたことで、DI や IoC を理解するのに非常に役立ちました。今回経験したような問題に直面することなく、このコラムをうまく活用していただければさいわいです。


Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (@julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Steve Smith に心より感謝いたします。
Steve Smith (@ardalis、英語) は、起業家であり、高品質ソフトウェアの開発に情熱を傾けるソフトウェア開発者でもあります。Steve は、DDD、SOLID、設計パターン、ソフトウェア アーキテクチャに関する複数のコースを Pluralsight で公開しています。彼は Microsoft MVP であり、開発者を対象としたカンファレンスで定期的に講演を行ったり、執筆者、指導者、およびトレーナーとしても活動しています。チームまたはプロジェクトに対して、Steve がどのような支援を提供しているかについては、ardalis.com (英語) を参照してください。