ASP.NET Web API 2 での依存関係の挿入
このチュートリアルでは、依存関係を ASP.NET Web API コントローラーに挿入する方法について説明します。
チュートリアルで使用するソフトウェアのバージョン
- Web API 2
- Unity アプリケーション ブロック
- Entity Framework 6 (バージョン 5 も機能します)
依存関係の挿入とは
"依存関係" とは、他のオブジェクトが必要とする任意のオブジェクトのことです。 たとえば、データ アクセスを処理するリポジトリを定義するのが一般的です。 例を挙げて説明します。 まず、ドメイン モデルを定義します。
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Entity Framework を使用して、データベースに項目を格納する単純なリポジトリ クラスを次に示します。
public class ProductsContext : DbContext
{
public ProductsContext()
: base("name=ProductsContext")
{
}
public DbSet<Product> Products { get; set; }
}
public class ProductRepository : IDisposable
{
private ProductsContext db = new ProductsContext();
public IEnumerable<Product> GetAll()
{
return db.Products;
}
public Product GetByID(int id)
{
return db.Products.FirstOrDefault(p => p.Id == id);
}
public void Add(Product product)
{
db.Products.Add(product);
db.SaveChanges();
}
protected void Dispose(bool disposing)
{
if (disposing)
{
if (db != null)
{
db.Dispose();
db = null;
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
次に、Product
エンティティの GET 要求をサポートする Web API コントローラーを定義しましょう。 (便宜上、POST や他の方法は省略します。)最初の試行は、次のとおりです。
public class ProductsController : ApiController
{
// This line of code is a problem!
ProductRepository _repository = new ProductRepository();
public IEnumerable<Product> Get()
{
return _repository.GetAll();
}
public IHttpActionResult Get(int id)
{
var product = _repository.GetByID(id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
コントローラー クラスは ProductRepository
に依存しており、コントローラーに ProductRepository
インスタンスを作成させることに注意してください。 ただし、いくつかの理由から、この方法で依存関係をハードコーディングすることはお勧めしません。
ProductRepository
を別の実装に置き換える場合は、コントローラー クラスも変更する必要があります。ProductRepository
に依存関係がある場合は、これらをコントローラー内で構成する必要があります。 複数のコントローラーを持つ大規模なプロジェクトの場合、構成コードはプロジェクト全体に分散されます。- コントローラーはデータベースに対してクエリを実行するようにハードコーディングされているため、単体テストは困難です。 単体テストでは、モックまたはスタブ リポジトリを使用する必要があり、現在の設計では不可能です。
これらの問題に対処するには、リポジトリをコントローラーに "挿入" します。 まず、ProductRepository
クラスをインターフェイスにリファクタリングします。
public interface IProductRepository
{
IEnumerable<Product> GetAll();
Product GetById(int id);
void Add(Product product);
}
public class ProductRepository : IProductRepository
{
// Implementation not shown.
}
次に、IProductRepository
をコンストラクター パラメーターとして指定します。
public class ProductsController : ApiController
{
private IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// Other controller methods not shown.
}
この例では、コンストラクター挿入を使用します。 "セッター挿入" を使用することもできます。ここでは、セッター メソッドまたはプロパティを使用して依存関係を設定します。
ただし、アプリケーションでコントローラーが直接作成されないため、ここで問題が発生します。 Web API は要求をルーティングするときにコントローラーを作成し、Web API は IProductRepository
について何も認識しません。 ここで、Web API 依存関係リゾルバーが役立ちます。
Web API 依存関係リゾルバー
Web API は、依存関係を解決するための IDependencyResolver インターフェイスを定義します。 インターフェイスの定義は次のとおりです。
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}
public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
IDependencyScope インターフェイスには、次の 2 つのメソッドがあります。
- GetService は、ある型の 1 つのインスタンスを作成します。
- GetServices は、指定した型のオブジェクトのコレクションを作成します。
IDependencyResolver メソッドは IDependencyScope を継承し、BeginScope メソッドを追加します。 スコープについては、このチュートリアルで後ほど説明します。
Web API はコントローラー インスタンスを作成するときに、最初に IDependencyResolver.GetService を呼び出し、コントローラーの型を渡します。 この機能拡張フックを使用して、コントローラーを作成し、依存関係を解決できます。 GetService が null を返した場合、Web API はコントローラー クラスでパラメーターなしのコンストラクターを検索します。
Unity コンテナーを使用した依存関係の解決
IDependencyResolver 実装をゼロから記述することもできますが、このインターフェイスは実際には Web API と既存の IoC コンテナーの間のブリッジとして機能するように設計されています。
IoC コンテナーは、依存関係の管理を担当するソフトウェア コンポーネントです。 コンテナーに型を登録し、コンテナーを使用してオブジェクトを作成します。 コンテナーは、依存関係を自動的に把握します。 多くの IoC コンテナーでは、オブジェクトの有効期間やスコープなどを制御することもできます。
Note
"IoC" とは、フレームワークがアプリケーション コードを呼び出す一般的なパターンである "Inversion of Control" (制御の反転) の略です。 IoC コンテナーによってオブジェクトが作成され、通常の制御フローが "反転" されます。
このチュートリアルでは、Microsoft Patterns & Practices の Unity を使用します。 (その他の一般的なライブラリには、Castle Windsor、Spring.Net、Autofac、Ninject、Simple インジェクターおよび StructureMap)。NuGet パッケージ マネージャーを使用して Unity をインストールできます。 Visual Studio の [ツール] メニューから、[NuGet パッケージ マネージャー] を選択し、[パッケージ マネージャー コンソール] を選択します。 [パッケージ マネージャー コンソール] ウィンドウで、次のコマンドを入力します。
Install-Package Unity
Unity コンテナーをラップする IDependencyResolver の実装を次に示します。
using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;
public class UnityResolver : IDependencyResolver
{
protected IUnityContainer container;
public UnityResolver(IUnityContainer container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
this.container = container;
}
public object GetService(Type serviceType)
{
try
{
return container.Resolve(serviceType);
}
catch (ResolutionFailedException exception)
{
throw new InvalidOperationException(
$"Unable to resolve service for type {serviceType}.",
exception)
}
}
public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return container.ResolveAll(serviceType);
}
catch (ResolutionFailedException exception)
{
throw new InvalidOperationException(
$"Unable to resolve service for type {serviceType}.",
exception)
}
}
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
container.Dispose();
}
}
依存関係リゾルバーの構成
グローバル HttpConfiguration オブジェクトの DependencyResolver プロパティに依存関係リゾルバーを設定します。
次のコードでは、IProductRepository
インターフェイスを Unity に登録し、UnityResolver
を作成します。
public static void Register(HttpConfiguration config)
{
var container = new UnityContainer();
container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
config.DependencyResolver = new UnityResolver(container);
// Other Web API configuration not shown.
}
依存関係スコープとコントローラーの有効期間
コントローラーは要求ごとに作成されます。 オブジェクトの有効期間を管理するために、IDependencyResolver は "スコープ" の概念を使用します。
HttpConfiguration オブジェクトにアタッチされている依存関係リゾルバーには、グローバル スコープがあります。 Web API はコントローラーを作成するときに、BeginScope を呼び出します。 このメソッドは、子スコープを表す IDependencyScope を返します。
その後、Web API は子スコープで GetService を呼び出し、コントローラーを作成します。 要求が完了すると、Web API は子スコープ上で Dispose を呼び出します。 Dispose メソッドを使用して、コントローラーの依存関係を破棄します。
BeginScope を実装する方法は、IoC コンテナーによって異なります。 Unity の場合、スコープは子コンテナーに対応します。
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
ほとんどの IoC コンテナーに、同様の相当するものがあります。