.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 されます。