.NET Framework の ASP.NET MVC5 アプリで Microsoft.Extensions.DependencyInjection 名前空間にあるクラスを利用して Dependency Injection (DI) 機能を実装してみましたので、忘れないように備忘録として残しておきます。

ターゲットフレームワークが .NET Framework 4.6.1 以降であれば ASP.NET Core で DI に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を利用できるそうなので試してみた次第です。
参考にしたのはググって探して GitHub に見つかった ASP.NET MVC and ServiceCollection sample という記事です。その記事のコードをほぼそのまま Global.asax.cs にコピーすれば必要最低限の DI 機能が実装できます。
ベ��スとした ASP.NET MVC5 アプリのプロジェクトは Visual Studio 2022 のテンプレートを使ってターゲットフレームワークを .NET Framework 4.8 として作成したものです。

そのプロジェクトに NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。(他に関係パッケージが 4 つ同時に自動的にインストールされます)

自動生成された Global.asax.cs を開き以下のように書き換えます。参考にした記事 ASP.NET MVC and ServiceCollection sample のコードほぼそのままです。自分なりに調べていろいろ分かったことをコメントとして追記してあります。
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Microsoft.Extensions.DependencyInjection;
using Mvc5DependencyInjection.Controllers;
using Mvc5DependencyInjection.Repositories;
// 下の設定により Application_Start より前に InitModule メソッド
// が実行され HTTP モジュール ServiceScopeModule が登録される
[assembly: PreApplicationStartMethod(
typeof(Mvc5DependencyInjection.MvcApplication), "InitModule")]
namespace Mvc5DependencyInjection
{
public class MvcApplication : System.Web.HttpApplication
{
public static void InitModule()
{
// HTTP モジュール ServiceScopeModule を登録
RegisterModule(typeof(ServiceScopeModule));
}
protected void Application_Start()
{
// DI コンテナの作成
var services = new ServiceCollection();
// DI コンテナにサービスを登録する。
// ConfigureServices メソッドは下のコード参照
ConfigureServices(services);
// サービスプロバイダの作成
ServiceProvider provider = services.BuildServiceProvider();
// HTTP モジュール ServiceScopeModule に上の provider を渡し、
// モジュール内でサービスプロバイダを利用できるようにする
ServiceScopeModule.SetServiceProvider(provider);
// 下の 4 行は Global.asax.cs にもともと含まれていたもの
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// SetResolver メソッドは Microsoft のドキュメントによると
// "Provides a registration point for dependency resolvers,
// using the specified dependency resolver interface." との
// こと。要するに、これにより MVC5 アプリで DI が実現できる
// ようになるらしい
var resolver = new ServiceProviderDependencyResolver();
DependencyResolver.SetResolver(resolver);
}
// DI コンテナにサービスを登録するメソッド。
// Controller については、ASP.NET Core では AddController,
// AddMvc, AddControllersWithViews, AddRazorPages などを使うと
// アセンブリをスキャンして全てを DI コンテナに登録するという
// ことが行われるようだが、それはこのサンプルでは未実装。
// すべて以下のメソッド内で登録する。
private void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ScopedThing>();
services.AddTransient<HomeController>();
}
}
// このサンプルではこのオブジェクトを HomeController に Inject
public class ScopedThing : IDisposable
{
public ScopedThing()
{
}
// Home/Index にてメッセージを取得できるように追加
public string GetMessage()
{
return "Message form ScopedThing";
}
public void Dispose()
{
}
}
// HTTP モジュール
// BeginRequest で IServiceScope を取得して HttpContext.Items
// に保持する。EndRequest で IServiceScope を Dispose すること
// により登録されたサービス (AddSingleton で登録されたものは除
// く) は Dispose される
internal class ServiceScopeModule : IHttpModule
{
private static ServiceProvider _serviceProvider;
// インターフェイスに定義されているので実装が必要
public void Dispose() { }
public void Init(HttpApplication context)
{
context.BeginRequest += Context_BeginRequest;
context.EndRequest += Context_EndRequest;
}
// IServiceScope オブジェクトを HttpContext.Items に保持
private void Context_BeginRequest(object sender, EventArgs e)
{
var context = ((HttpApplication)sender).Context;
context.Items[typeof(IServiceScope)] =
_serviceProvider.CreateScope();
}
// IServiceScope オブジェクトを Dispose する
private void Context_EndRequest(object sender, EventArgs e)
{
var context = ((HttpApplication)sender).Context;
if (context.Items[typeof(IServiceScope)]
is IServiceScope scope)
{
scope.Dispose();
}
}
// Application_Start でサービスプロバイダが渡される
public static void SetServiceProvider(
ServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
}
// BeginRequest で IServiceScope が HttpContext.Items に保持され
// ているので、それを使って DI に登録されたサービスを取得できる
internal class ServiceProviderDependencyResolver
: IDependencyResolver
{
public object GetService(Type serviceType)
{
if (HttpContext.Current?.Items[typeof(IServiceScope)]
is IServiceScope scope)
{
return scope.ServiceProvider.GetService(serviceType);
}
throw new InvalidOperationException(
"IServiceScope not provided");
}
public IEnumerable<object> GetServices(Type serviceType)
{
if (HttpContext.Current?.Items[typeof(IServiceScope)]
is IServiceScope scope)
{
return scope.ServiceProvider.GetServices(serviceType);
}
throw new InvalidOperationException(
"IServiceScope not provided");
}
}
}
上の ConfigureServices メソッドに Controller とそれが依存するクラスを登録すれば DI 機能は働くようになります。
この記事では HomeController と ScopedThing を登録しています。自動生成された HomeController のコードに手を加えて、以下のようにコンストラクタ経由で ScopedThing を受け取れるようにすれば、クライアントからの要求を受けて HomeController が呼び出されたときに DI 機能により ScopedThing のインスタンスへの参照がコンストラクタの引数に渡されます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Mvc5DependencyInjection.Controllers
{
public class HomeController : Controller
{
private readonly ScopedThing _scopedThing;
public HomeController(ScopedThing scopedThing)
{
this._scopedThing = scopedThing;
}
public ActionResult Index()
{
string msg = _scopedThing.GetMessage();
ViewBag.Msg = msg;
return View();
}
}
}
Index アクションメソッドに書いたように、Inject された ScopedThing インスタンスの GetMessage メソッドで "Message form ScopedThing" という文字列を取得できます。
処理が終わると HttpApplication.EndRequest イベントが発生するので、HTTP モジュール ServiceScopeModule により ScopedThing の Dispose メソッドが自動的に呼ばれます。
先の記事「ASP.NET MVC5 で Autofac.Mvc5 使って DI」と同様に、下の画像のようなリポジトリパターンを実装し、「Entity データモデル」を「本番用クラス」に、「本番用クラス」を「コントローラークラス」に Inject することもできます。

具体例を書くと、まず、先の記事と同様に (1) Visual Studio の ADO.NET Entity Data Model ウィザードで Entity Data Model (EDM) を作成し、(2) それをベースにスキャフォールディング機能を使って CRUD 用の Controller を自動生成させ、(3) Controller から SQL Server にアクセスして操作するコードをリポジトリクラスに切り出し、(4) Controller にリポジトリクラスを Inject できるようにコンストラクタを追加します。その手順の詳しい説明とコード例は先の記事にありますので見てください。
その後、作成した EDM のコンテキストクラス、リポジトリクラス、コントローラークラス を上の Global.asax.cs の ConfigureServices メソッドで DI コンテナに登録します。以下のコードで「// 追加」とコメントした下の 3 行がそれです。それだけで DI 機能が働くようになります。
private void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ScopedThing>();
services.AddTransient<HomeController>();
// 追加
services.AddScoped<NORTHWINDEntities>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddTransient<ProductsController>();
}
上の画像の「コントローラークラス」が ProductsController に、「インターフェイス」が IProductRepository に、「本番用クラス」が ProductRepository に、「Entity データモデル」が NORTHWINDEntities に該当します。
ブラウザから Prodtcuts/Index を要求すると、ProductsController が依存する ProductRepository、ProductRepository が依存する NORTHWINDEntities は DI 機能により自動的に Inject され、SQL Server からデータを取得して結果が表示されます。それがこの記事の一番上の画像です。