Edited at

Autofac について調べてみた その3 WebApi とのインテグレーション

More than 1 year has passed since last update.

WebApi のインテグレーションがあるようなので、試してみた。私は、C# に慣れていないので、いくつかつまづいたが、それを含めてシェアしたい。今回は、私のメインマシンが壊れたため、Visual Studio for Mac でコーディングしてみた。.NET がしっかり動作した。今回のコードは、こちらのリポジトリに置いている。


Visual Studio for Mac での準備事項

次の方法で、Visual Studio for Mac での環境を整えた。


ASP.NET のプロジェクトを作成


WebApi を選択

複雑さを低減するために、最も単純にした。


プロジェクト名の指定


Autofac のインストール

Packages を右クリックして、Autofac.WebApi2 で検索して、インストールする。ただし、そのままではうまく動作しなかった。特定のメソッドが見つからない。どうやら、Autofac.WebApi2 の依存するAutofac が古いらしい。(3.5) インストール後、Package を右クリック して、Update するとワーニングは出るが、動作はする。現在のコードをみると、Autofac がバージョン4.6.0 になっている様子。ContainerBuilder#Register で依存が3.5である旨のワーニングが出るが、動作には今のところ支障はない様子。Windows だったらこの辺りは出なかった。


WebApi アプリケーションの作成

Visual Studio Community for Mac では、Windows の Visual Studio ほど賢くないので、Controller のテンプレートなどは存在しない。そのままディレクトリをほって、クラスを書く。

今回書いてみたコードはこんなの。

ProductsContorller.cs

using System;

using System.Collections.Generic;
using System.Net;
using System.Web.Http;
using WebApiSample.Models;
using System.Linq;

namespace WebApiSample.Controllers
{
public class ProductsController : ApiController
{
Product[] products = new Product[]
{
new Product(){ Id = 1, Name = "Curry", Category = "Dinner", Price = 1},
new Product(){ Id = 2, Name = "Tandory Chicken", Category = "Sub", Price = 2},
new Product(){ Id = 3, Name = "Mouse", Category = "PC", Price = 10000}
};

public IEnumerable<Product> GetAllProducts()
{
return products;
}

public IHttpActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
}

Product.cs

using System;

namespace WebApiSample.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
}

とても一般的なコードだ。ここに、Autofac のDI の設定を入れる。

Global.asax.cs に次のコードを書く。


protected void Application_Start()
{
var builder = new ContainerBuilder();
var config = GlobalConfiguration.Configuration;
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); // アセンブリの中のコントローラを取得してくれる。
builder.RegisterWebApiFilterProvider(config); // オプションで、フィルタのプロバイダを登録する(オプション)
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
} // 依存性を解決するためのプロパティにAutofacWebApi のリゾルバを追加する。
GlobalConfiguration.Configure(WebApiConfig.Register); // ルーティングの処理などを書く

これだけで、すでに、Autofac が追加された上で、WebApiが動作している。Visual Studio for Mac の実行ボタンを押して、http://localhost:xxxx/api/products や、http://localhost:xxxx/api/product/1 などにアクセスすると、動作しているのがわかるだろう。


コントローラーの直接登録

先ほどは、アセンブリから、コントローラのリストを取得して、登録していたが、個別にコントローラを登録する方法がある。

builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); 

こちらの方法だ

builder.RegisterType<ProductsController>().InstancePerRequest();

これは通常の DI の書き方そのものだ。


フィルタの追加

さて、このままだと、DI の旨味が全く感じられないので、フィルタを作ってみる。ログをコンソールに出力するフィルタを書いてみる。それをDIしてみる。

LoggingActionFilter.cs

using System;

using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Autofac.Integration.WebApi;

namespace WebApiSample.Filters
{
public class LoggingActionFilter : IAutofacActionFilter
{
readonly ILogger logger;

public LoggingActionFilter(ILogger logger) {
this.logger = logger;
}

public Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
logger.Write(actionExecutedContext.ActionContext.ActionDescriptor.ActionName);
return Task.FromResult(0);
}

public Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
logger.Write(actionContext.ActionDescriptor.ActionName);
return Task.FromResult(0);
}
}

public interface ILogger
{
void Write(string message);
}

public class ConsoleLogger : ILogger
{

public void Write(string message)
{
Console.WriteLine("[Logging]:" + message);
}
}
}

ILogger インターフェイスと、その実装 ConsoleLogger を提供する。IAutofacActionFilter を実装したクラスを作成してフィルタのインジェクションの準備環境。

Global.asax.cs を変更する。

        protected void Application_Start()

{
var builder = new ContainerBuilder();
var config = GlobalConfiguration.Configuration;
builder.RegisterWebApiFilterProvider(config);

builder.RegisterType<ProductsController>().InstancePerRequest();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger>()))
.AsWebApiActionFilterFor<ProductsController>(c => c.GetProduct(default(int))) .InstancePerRequest();

var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

GlobalConfiguration.Configure(WebApiConfig.Register);
}

ILogger を登録してあげて、ContainerBuilder#Register のラムダ式を使って、フィルタをかけたいメソッドを登録している。これで、アプリを実行すると、動作は変わらないが、Application Output をみると

#[Logging]:GetProduct

#[Logging]:GetProduct

と出力される。GetAllProducts を実行したとしても、GetProduct にしかフィルターがかかっていないからこのような動作になる。


複数のフィルタ適用

では、複数のメソッドにフィルタをかけたい場合はどうするのだろう? すべてのメソッドが対象なら、ラムダ式を書かなければ良い。

builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger>()))

.AsWebApiActionFilterFor<ProductsController>().InstancePerRequest();

実行結果

#[Logging]:GetAllProducts

#[Logging]:GetAllProducts
#[Logging]:GetProduct
#[Logging]:GetProduct

じゃあ、複数のメソッドや、複数のコントローラにかけたい場合は?単に列挙すれば良い。

builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger>()))

.AsWebApiActionFilterFor<ProductsController>(c => c.GetProduct(default(int)))
          .InstancePerRequest();
builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger>()))
.AsWebApiActionFilterFor<ProductsController>(c => c.GetAllProducts())
.InstancePerRequest();

実行結果

#[Logging]:GetAllProducts

#[Logging]:GetAllProducts
#[Logging]:GetProduct
#[Logging]:GetProduct

以上です