ASP.NET Web Api における IoC (関心事の分割)

(2012/03 : サンプル コードを、WCF Web Api から ASP.NET Web Api に変更)

(2012/06 : サンプル コードを、ASP.NET Web API RC 版にあわせて変更)

環境 : Visual Studio 2010, ASP.NET Web API RC (ASP.NET MVC 4 RC), Autofac for .NET 4 (Release 2.6.1)

REST サービス / Web Api の実践

ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「Getting Started with ASP.NET Web Api」 (ASP.NET の場合)、または「REST サービスの作成」 (WCF の場合) を参照してください。

こんにちは。

ちょっとこむずかしいタイトルですみませんが、今回は、ASP.NET Web API のメリットの 1 つである「処理の分離」について記載します。

2015/03 追記 : ASP.NET 5 では DI (Dependency Injection) が Built-in される予定です。(ここでは、Autofac を組み合わせて実装しています。)

 

メッセージ ハンドラー (HttpMessageHandler)

これまで、HTTP ヘッダーのカスタマイズETag を使った排他処理 など、さまざまな処理を Web Api のロジックに混ぜて記載してきました。しかし、こうした汎用性の高い処理は、Web Api のロジックと分離した形で構築 (もしくは、ライブラリー化) をおこない、組み合わせ (Assemble) したいですね。
ASP.NET Web API を使用すると、こうした Middler tire の処理をカスタマイズするための HttpMessageHandler (DelegatingHandler) を構築して、プラグインできます。

では、こちら で作成した OrderController のサンプル コード (Expires ヘッダーを追加するサンプル コード) を、今回は、カスタム ハンドラーを使って処理してみましょう。

まず、ハンドラー クラスを作成します。
プロジェクトに、クラスを新規追加します。(今回、このファイル名を CustomHeaderMessageHandler.cs とします。) 作成されたクラス ファイルに、下記の通り、DelegatingHandler クラスを継承したコードを記述します。

. . .
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
. . .

public class CustomHeaderMessageHandler : DelegatingHandler
{
  public CustomHeaderMessageHandler() { }

  protected override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
  {
    Task<HttpResponseMessage> t = base.SendAsync(request, cancellationToken);
    return t.ContinueWith(
      task =>
      {
        task.Result.Headers.CacheControl =
          new System.Net.Http.Headers.CacheControlHeaderValue() { Private = true };
        task.Result.Content.Headers.Expires = DateTimeOffset.UtcNow.AddMinutes(1);
        return task.Result;
      },
      TaskContinuationOptions.ExecuteSynchronously);
  }
}
. . .

DelegatingHandler クラスの override メソッドで重要なメソッドは、上記の SendAsync メソッドです。登録されたハンドラーが処理される際に、この SendAsync が呼び出されます。

なお、上記の通り、base.SendAsync は必ず呼び出すようにしてください。(この呼び出しで、他のメッセージ ハンドラーも引き続き呼び出されるようになります。) 上記では、base.SendAsync を呼び出して、その結果の HttpResponseMessage オブジェクトに、同期的に (順番に)、Expires ヘッダーを追記しています。(もちろん、他の処理と競合しないなら、ContinueWith を使わず、非同期で処理しても構いません。)
今回は HttpResponseMessage オブジェクトを変更していますが、引数で渡されている HttpRequestMessage を変更し、Web Api に渡すリクエストの内容をカスタマイズすることもできます。

では、上記のメッセージ ハンドラー (CustomHeaderMessageHandler) を Web Api に組み込んでみましょう。
プロジェクトの App_Start/WebApiConfig.cs を開き、下記 (太字) の通り、追記します。ここでも、「Web Api (REST サービス) で Custom MIME タイプを処理する」で使用した HttpConfiguration を使用して、Web Api の構成情報をカスタマイズします。今回は、HttpConfiguration の MessageHandlers プロパティ (コレクション) を使用して、上記のメッセージ ハンドラーを追加しています。

. . .

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    . . .
   config.MessageHandlers.Add(new CustomHeaderMessageHandler());
    . . .

今回は単にヘッダーを追加しましたが、この仕組みを使って、例えば、ETag の処理をおこなったり、Path (URI の Segment) に応じた独自の処理をおこなったり (例えば、http://…/xml、http://…/json、http://…/atom など、URI Segment に応じて Accept フォーマットをカスタマイズしたり)、バージョンに応じて URI のマッピングをおこなう など、さまざまなカスタマイズを、Web Api のロジックと分離した形で構築して、上記のようにプラグインできます。

 

IoC コンテナと DependencyResolver

上記のメッセージ ハンドラーだけでは、分離がむずかしい場合もあります。
典型的なパターンとして、Web Api の操作 (GET, POST, PUT, DELETE …) と、データベースなどへの永続化の処理を関連付けるような場合です。こうした永続化 (Persistence) の処理は、各操作のロジックと密接に関連しているため、上述のようにメッセージ ハンドラーを使ってきれいに分離することは困難です。

こうした場合に使用できるのが、HttpConfiguration オブジェクトの DependencyResolver です。
DependencyResolver を使うと、Web Api (REST サービス) が呼び出されてインスタンス化する際など、ASP.NET Web API のフレームワークに組み込まれた さまざまな処理をカスタマイズできます。Autofac、Ninject、MEF などの IoC コンテナ (Inversion of Control) と組み合わせることで、Web Api で使用される各サービスを関連付けることが可能です。

では、実際に、こちら で作成した OrderItem、OrcderController を使用して、永続化リポジトリーの分離の例を示しましょう。なお、今回は、IoC Container として、Open Project の Autofac を使用します。

補足 : Autofac 自体も、ASP.NET MVC、WCF、MEF など、さまざまな .NET の要素技術と連携する仕組みを持っています。今回は、Autofac の基本的な仕組みだけを使用します。

まず、クラスを追加して、下記の通り、ISampleRepository インタフェースを作成します。
データベースの種類に応じた永続化処理は、この ISampleRepository インタフェースを実装 (implement) したクラスを提供して、Web Api にプラグインします。その実体 (implementation) の 1 つである SampleRepository1 も、下記の通り、同時に作成しておきます。

補足 : 実際の開発では、下記の OrderItem というカスタム クラス自体も、Generic を使用するなど、Web Api に依存しないよう設計しておきましょう。(例えば、ISampleRepository<T> など。) 今回は、サンプルとして、そのまま OrderItme を使用します。。。

. . .

public interface ISampleRepository
{
  OrderItem Get(int id);
  void Insert(OrderItem item);
  void Update(OrderItem item);
  void Delete(int id);
}

public class SampleRepository1 : ISampleRepository
{
  public OrderItem Get(int id)
  {
    OrderItem resObj = null;

    // Search from database (this time, we skip code . . .)
    . . .

    return resObj;
  }

  public void Insert(OrderItem item)
  {
    throw new NotImplementedException();
  }

  public void Update(OrderItem item)
  {
    throw new NotImplementedException();
  }

  public void Delete(int id)
  {
    throw new NotImplementedException();
  }
}

. . .

OrderController のコンストラクタを変更し、下記 (太字) の通り、ISampleRepository インタフェースと関連付けができるようにしておきます。

. . .

public class OrderController : ApiController
{
  private ISampleRepository rep;

  public OrderController(ISampleRepository arg)
  {
    this.rep = arg;
  }

  // GET /api/order/5
  public HttpResponseMessage Get(int id)
  {
    // get data using ISampleRepository obejct
    OrderItem resObj = rep.Get(id);

    HttpResponseMessage resMsg =
      new HttpResponseMessage(HttpStatusCode.OK);     
    resMsg.Content = new ObjectContent<OrderItem>(
      resObj,
      new JsonMediaTypeFormatter());
    return resMsg;
  }
  . . .
}
. . .

さいごに、Autofac を使って、上記のリポジトリーの実装 (SampleRepository1) と Web Api (OrderController) の関連付けをおこないます。
Autofac のアセンブリ (Autofac.dll) の参照追加をおこない、App_Start/WebApiConfig.cs を開いて、下記 (太字) の通りコードを追記します。

. . .
using Autofac;

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    . . .
    ContainerBuilder cb = new ContainerBuilder();
    cb.RegisterType<MvcApplication1.Controllers.OrderController>();
    cb.RegisterType<SampleRepository1>().As<ISampleRepository>();
    config.DependencyResolver = new MyDependency(cb.Build());
  }
}

public class MyDependency :
  System.Web.Http.Dependencies.IDependencyResolver
{
  private IContainer _container;

  public MyDependency(IContainer container)
  {
    this._container = container;
  }

  public System.Web.Http.Dependencies.IDependencyScope
    BeginScope()
  {
    return this;
  }

  public object GetService(Type serviceType)
  {
    return _container.IsRegistered(serviceType) ?
      _container.Resolve(serviceType) :
      null;
  }

  public IEnumerable<object> GetServices(Type serviceType)
  {
    return new object[] { }; // do nothing
  }

  public void Dispose()
  {
  }
}
. . .

Web Api のフレームワークの中で呼び出される各種オブジェクトは、「サービス」 (Service) と呼ばれます。今回の場合、OrderController 自体もサービスの 1 つです。そして、上記の IDependencyResolver を使うと、この各サービスのインスタンス化の処理をカスタマイズできます。(各サービスは、クライアントからの Web Api の呼び出しの際に、都度、呼び出されます。)
OrderController の Api が呼び出されると、Autofac の Resolve メソッドにより、ビルドされた OrderController がインスタンス化されます。この際、ISampleRepository (SampleRepository1 オブジェクト) がコンテナにビルドされているため、Autofac により、前述のコンストラクタ (ISampleRepository を引数に持つコンストラクタ) が呼び出され、SampleRepository1 のインスタンスが引数に渡されます。

Controller のように、単一のオブジェクトを返すようなサービスでは、上記の GetService が呼ばれます。また、複数のオブジェクトを返すようなサービス (IFilterProvider) では、上記の GetServices が呼ばれます。ASP.NET Web API では、下記の各サービスが呼び出されます。

単一のサービス (GetService)

  • System.Web.Http.Dispatcher.IHttpControllerFactory
  • System.Web.Http.Common.ILogger
  • System.Web.Http.Dispatcher.IHttpControllerActivator
  • System.Web.Http.Controllers.IHttpActionSelector
  • System.Web.Http.Controllers.IHttpActionInvoker
  • System.Net.Http.Formatting.IFormatterSelector
  • ApiController 自身 (今回の場合、OrderController)

複数のサービス (GetServices)

  • System.Web.Http.Filters.IFilterProvider

また、上記のすべてのサービスを実装する必要はなく、Dependency が解決できない場合は、 GetService では null を返し、GetServices では IEnumerable<object>(0) を返すようにします。(ASP.NET Web API では、既定の DefaultServiceResolver により、いくつかのサービスが設定されています。null や IEnumerable<object>(0) を返すと、この既定のサービスが呼び出されます。)

補足 : これは RC 版のバグかもしれませんが、Controller については、一度 null を返すと、次回以降、すべての Controller について依存関係を解決しなくなってしまいます。このため、Controller のみ、とりあえず、使用する Controller をすべて入れておきましょう。

例えば、下記の通り記述すると、IFilterProvider の取得の際に、FilterProvider1、FilterProvider2 の 2 つの要素の IEnumerable (複数のフィルター要素) が解決されて返されます。(既定では、属性 (Attribute) として設定されたフィルターを取得する ActionDescriptorFilterProvider や、HttpConfiguration に設定されたフィルターを取得する ConfigurationFilterProvider などが使用されます。)

. . .

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    . . .
    ContainerBuilder cb = new ContainerBuilder();
    cb.RegisterType<FilterProvider1>().As<IFilterProvider>();
    cb.RegisterType<FilterProvider2>().As<IFilterProvider>();
    . . .
    config.DependencyResolver = new MyDependency(cb.Build());
    . . .
  }
}

public class MyDependency :
  System.Web.Http.Dependencies.IDependencyResolver
{
  private IContainer _container;

  public MyDependency(IContainer container)
  {
    this._container = container;
  }

  public System.Web.Http.Dependencies.IDependencyScope
    BeginScope()
  {
    return this;
  }

  public object GetService(Type serviceType)
  {
    return _container.IsRegistered(serviceType) ?
      _container.Resolve(serviceType) :
      null;
  }

  public IEnumerable<object> GetServices(Type serviceType)
  {
    Type e = typeof(IEnumerable<>).MakeGenericType(serviceType);
    // This returns 2 instances.
    return ((IEnumerable) _container.Resolve(e)).Cast<object>();
  }

  public void Dispose()
  {
  }
}
. . .

また、ASP.NET Web API RC 版から、上記の通り BeginScope メソッドが追加されています。このメソッドは、Container などのライフサイクルを管理するためのものです。
BeginScope は、Web API が呼び出されるたびに (Controller が呼び出れるたびに)、毎回 呼び出されます。そして、依存性を解決する際に、ここで返された IDependencyScope オブジェクトの GetService メソッド、GetServices メソッドが使用されます。(上記では this を返していますが、実は、IDependencyResolver 自体が IDependencyScope を継承しています。) そして、Web API の呼び出しが完了すると、この IDependencyScope オブジェクトの Dispose メソッドを呼びます。
ご存じの通り、Application_Start は Web アプリケーションの開始時に呼ばれるため、上記のコードの Container (変数 _container) は、アプリケーションが起動している間、ずっと保持されます。例えば、これを、Controller の呼び出しがおこなわれるたびに、毎回 container を作成して、都度破棄したい場合などに、この BeginScope が使用できます。

ここでは、Autofac を例に使用しましたが、もちろん、.NET 標準の MEF、Ninject などの IoC を使うこともできます。また、上記は簡単なサンプルで例示しましたが、Autofac などが持つさらに高度な仕組みと組み合わせ、より高度な処理の分割が可能です。

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s