.NET Framework 4.8 の ASP.NET MVC5 アプリで Autofac.Mvc5 を利用して Dependency Injection (DI) 機能を実装してみました。忘れないように備忘録として残しておきます。
Visual Studio のテンプレートで作る ASP.NET MVC5 プロジェクトには DI 機能は実装されていません。Microsoft のドキュメント「ASP.NET MVC と ASP.NET Core での依存関係の挿入の相違点」にサードパーティ製の Autofac が紹介されていましたので使ってみました。
(ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は .NET Framework 4.6.1 以降であれば利用できるそうなので、そちらを使うことを考えた方がいいかもしれません)
ベースとした ASP.NET MVC5 アプリは、先の記事「スキャフォールディング機能」に書いたものと同じです。Microsoft のサンプル SQL Server データベース Northwind から Entity Data Model (EDM) を作り、スキャフォールディング機能を使って Create, Read, Update, Delete (CRUD) 操作を行う Controller と View を一式自動生成しています。
スキャフォールディング機能で自動生成されたコードに手を加えてリポジトリパターンを使うように変更し、下の画像の「本番用クラス」とそれが使う EDM のコンテキストクラスを DI 機能を使って Inject できるようにしてみます。
自動生成される Controller のコードは、内部で以下のようにコンテキストクラス NORTHWINDEntities のインスタンスを生成し、それを使って Linq to Entities で SQL Server にアクセスして操作するコードがハードコーディングされています。まず、その部分のコードを「本番用クラス」に切り出します。
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
namespace Mvc5App2.Controllers
{
public class ProductsController : Controller
{
private NORTHWINDEntities db = new NORTHWINDEntities();
// GET: Products
public async Task<ActionResult> Index()
{
var products = db.Products
.Include(p => p.Categories)
.Include(p => p.Suppliers);
return View(await products.ToListAsync());
}
// ・・・中略・・・
}
}
上の図の「インターフェイス」は IProductRepository という名前で以下のようにしました。Controller には Index, Details, Create, Edit, Delete アクションメソッドがありますので、IProductRepository にはそれらが使うメソッドをすべて定義しています。非同期操作を行うので戻り値は Task<T> としています。
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Mvc5AppAutofac.Models
{
public interface IProductRepository
{
Task<IEnumerable<Products>> GetProducts();
Task<Products> GetProductById(int id);
Task<IEnumerable<Categories>> GetCatagories();
Task<IEnumerable<Suppliers>> GetSuppliers();
Task<int> CreateProduct(Products product);
Task<int> UpdateProduct(Products product);
Task<int> DeleteProduct(int id);
}
}
上の図の「本番用クラス」は上の IProductRepository インターフェイスを継承し、ProductRepository という名前で以下のようにしました。コンテキストクラス NORTHWINDEntities は DI 機能を使ってコンストラクタ経由で Inject することを考えています。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Threading.Tasks;
namespace Mvc5AppAutofac.Models
{
public class ProductRepository : IProductRepository,
IDisposable
{
private readonly NORTHWINDEntities db;
// Dispose パターンの実装のための変数
private bool disposedValue;
public ProductRepository(NORTHWINDEntities db)
{
this.db = db;
}
public async Task<IEnumerable<Products>> GetProducts()
{
var products = db.Products
.Include(p => p.Categories)
.Include(p => p.Suppliers);
return await products.ToListAsync();
}
public async Task<Products> GetProductById(int id)
{
Products product = await db.Products.FindAsync(id);
return product;
}
public async Task<IEnumerable<Categories>> GetCatagories()
{
var categgories = db.Categories;
return await categgories.ToListAsync();
}
public async Task<IEnumerable<Suppliers>> GetSuppliers()
{
var suppliers = db.Suppliers;
return await suppliers.ToListAsync();
}
public async Task<int> CreateProduct(Products product)
{
db.Products.Add(product);
return await db.SaveChangesAsync();
}
public async Task<int> UpdateProduct(Products product)
{
db.Entry(product).State = EntityState.Modified;
return await db.SaveChangesAsync();
}
public async Task<int> DeleteProduct(int id)
{
Products products = await db.Products.FindAsync(id);
db.Products.Remove(products);
return await db.SaveChangesAsync();
}
// 以下は Dispose パターンの実装
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (db != null)
{
db.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
上の ProductRepository クラスは IDisposable インターフェイスも継承していますが、その理由を以下に説明します。
ProductRepository クラスは、DI 操作によりコンストラクタ経由で NORTHWINDEntities クラスのインスタンスへの参照を受け取り、それを変数 db に保持します。NORTHWINDEntities クラスは DbContext クラスを継承しており、DbContext クラスは IDisposable インターフェイスを継承していますので、使い終わったら Dispose する必要があります。
そのために、ProductRepository クラスには IDisposable インターフェイスも継承させ、Dispose パターンを実装してその中で NORTHWINDEntities オブジェクトを Dispose するようにしています。
ProductRepository クラスの Dispose メソッドは、Autofac のドキュメント Disposal の Automatic Disposal のセクションに書いてあるように、DI 機能により生成されたインスタンスの lifetime の終わりに自動的に呼び出されるそうです。デバッガを使って実際に呼び出されることは確認できました。
自動生成された Controller のコードを、DI 機能を利用してコンストラクタ経由で上の ProductRepository クラスのインスタンスへの参照を受け取れるように変更し、ProductRepository クラスに実装されたメソッドを使って SQL Server にアクセスして必要な操作ができるように書き換えます。
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5AppAutofac.Models;
namespace Mvc5AppAutofac.Controllers
{
public class ProductsController : Controller
{
private readonly IProductRepository rep;
public ProductsController(IProductRepository rep)
{
this.rep = rep;
}
// GET: Products
public async Task<ActionResult> Index()
{
return View(await rep.GetProducts());
}
// ・・・中略・・・
}
}
最後に、この記事の一番上の画像の Autofac.Mvc5 v6.1.0 を NuGet からインストールし、その DI 機能が働くように設定します。。
そのためには、Controller, ProductRepository, NORTHWINDEntities を DI コンテナに含めて初期化し、ASP.NET に登録する必要があります。具体的には、Global.asax にある既存の Application_Start メソッドに「Autofac.Mvc5 による DI を行うため以下のコードを追加」とコメントした下のコードを追加します。
using Autofac;
using Autofac.Integration.Mvc;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Mvc5AppAutofac.Models;
namespace Mvc5AppAutofac
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// Autofac.Mvc5 による DI を行うため以下のコードを追加
// DI コンテナを作成するビルダのインスタンスを生成
var builder = new ContainerBuilder();
// アセンブリをスキャンしてすべての Controller を DI コン
// テナに登録。下のコードの MvcApplication は Global.asax
// のクラス名。スコープを指定しない場合はデフォルトの
// InstancePerDependency になるらしい
builder.RegisterControllers(typeof(MvcApplication).Assembly)
.InstancePerRequest();
// ProductRepository クラスと NORTHWINDEntities クラスを
// DI コンテナに登録。スコープを指定しない場合はデフォル
// トの InstancePerDependency になる
builder.RegisterType<ProductRepository>()
.As<IProductRepository>()
.InstancePerRequest();
builder.RegisterType<NORTHWINDEntities>()
.InstancePerRequest();
// DI コンテナの生成
var container = builder.Build();
// DI コンテナを ASP.NET に登録
DependencyResolver.SetResolver(
new AutofacDependencyResolver(container));
}
}
}
設定の説明は上のコードに付与したコメントを見てください。詳細が必要でしたら Autofac のドキュメント MVC を見てください。
ASP.NET Core に組み込みの DI 機能には DI により生成されたインスタンスの lifetime を、DI コンテナの登録する際に AddTransient, AddScoped, AddSingleton の 3 種類のメソッドを使って設定できますが、それと同様な機能は Autofac にもあります。詳しくは Autofac のドキュメント Instance Scope を見てください。
上のコード例では InstancePerRequest に設定していますが、それは ASP.NET Core 組み込みの DI 機能では AddScoped に相当します。これにより、要求ごとに DI コンテナからインスタンスが生成され、応答を返すと廃棄されます。廃棄される際、上の ProductsController クラスに実装した Dispose メソッドが呼び出されます。
以上により、ASP.NET が Controller のインスタンスを作る際 DI 機能が働いて、自動的に ProductRepository, NORTHWINDEntities クラスのインスタンスが生成され、それらへの参照がコンストラクタ経由で inject されます。