ASP.NETにおけるDIのおさらい
例えば以下のようなインターフェースとその実装クラスを考えてみます。
namespace Hoge.Services.Sample
{
public interface ISampleService
{
void Fuga();
}
public class SampleService : ISampleService
{
public SampleService() { }
public void Fuga()
{
// do something
}
}
}
ASP.NETでDIを利用するときは、このインターフェースと実装クラスの関係をStartup.csなどでサービスコンテナーに追加してあげるのが一般的かなと思います。
Microsoft公式のDIの説明
namespace Hoge
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ISampleServiceのDIを登録
services.AddScoped<ISampleService, SampleService>();
services.AddControllersWithViews();
}
}
}
このようにしておくことでコントローラーからはISampleServiceを以下のように利用することができます。
namespace Hoge.Controllers
{
public class SampleController : Controller
{
private readonly ISampleService _sampleService;
// コンストラクタでDIされたISampleServiceのインスタンスを受け取る
public HomeController(ISampleService sampleService)
{
_sampleService = sampleService;
}
public void Piyo() {
_sampleService.Fuga();
}
何が問題になるのか
上記の例だとサービスコンテナーにISampleServiceの依存関係を1つ登録するだけでしたが、実際のシステムとなるとそうはいきません。
システムの規模にもよりますが、数十から数百の依存関係を定義することになってもおかしくないです。
デザインパターンとしてリポジトリパターンを採用していたりすると、Service層だけでなくRepository層やModel層で定義したクラスにも依存関係を登録する必要が出てきます。
そうなるともう依存関係の登録だけでコード量が膨大になり、管理も大変になることは間違いないです。
実際過去にSpring Frameworkを使った案件では、xmlにDIの定義をずらずら書いていた記憶があります。
namespace Hoge
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 依存関係の登録が膨大になってしまう...!
services.AddScoped<ISampleService, SampleService>();
services.AddScoped<IHogeService, HogeService>();
services.AddScoped<IFugaService, FugaService>();
services.AddScoped<ISampleRepository, SampleRepository>();
services.AddScoped<ISampleModel, SampleModel>();
.
.
.
services.AddControllersWithViews();
}
}
}
DI定義の登録を自動化したい..!
そこで考えたのが今回紹介する方法です。
とりあえず実装を見てもらえば解説はそんなに必要ないかと思うので、サンプルコードを記載します。
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using System.Reflection;
namespace Hoge.DependencyInjections
{
public static class ServiceDependencyCollections
{
// IServiceCollectionの拡張メソッドとして作成することでStartup.csのConfigureServicesから呼び出せる
public static IServiceCollection AddServiceDependencyCollections(this IServiceCollection services)
{
// DIが必要なインターフェースが属するnamespace
string[] nameSpaces = { $"{nameof(Hoge)}.{nameof(Hoge.Services)}",
$"{nameof(Hoge)}.{nameof(Areas)}.{nameof(Areas.Fuga)}.{nameof(Areas.Fuga.Services)}"};
foreach (var nameSpace in nameSpaces)
{
// リフレクションを使って指定したnamespaceに属するインターフェースを取得します
var serviceInterfaces = Assembly.GetExecutingAssembly().GetTypes()
.Where(e => e.IsInterface && e.FullName!.StartsWith(nameSpace));
// 同じくリフレクションで指定のnamespaceに属するクラスを取得
var serviceClasses = Assembly.GetExecutingAssembly().GetTypes()
.Where(e => e.IsClass && e.FullName!.StartsWith(nameSpace));
foreach (var serviceInterface in serviceInterfaces)
{
// 取得したインターフェースでループして、実装クラスが見つかればDI登録
var implementationType = serviceClasses.FirstOrDefault(c => c.GetInterfaces().Contains(serviceInterface));
if (implementationType != null)
{
services.AddScoped(serviceInterface, implementationType);
}
}
}
return services;
}
}
}
using Hoge.DependencyInjections;
namespace Hoge
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ServiceDependencyCollectionsの呼び出し
services.AddServiceDependencyCollections();
services.AddControllersWithViews();
}
}
}
ServiceDependencyCollections.csで指定したnamespaceに属するインターフェースを取得し、そのインターフェースの実装クラスが見つかればDI登録しています。
AddServiceDependencyCollectionsはIServiceCollectionの拡張メソッドとして作成しているので、Startup.csからservices.AddServiceDependencyCollections();
のように呼び出すことができます。
この方法であれば指定したnamespace配下のインターフェースを全て自動でDIに追加することができるので、Startup.csに依存関係をすべて記述する必要はなくなり、管理は大幅に楽になります!
DI登録を忘れてデバッグ起動してInvalidOperationExceptionでやり直し、みたいなことからもオサラバです。
今回はService層用のServiceDependencyCollections.csだけサンプルコードを載せていますが、Repository層、Model層用のクラスも同じように作成しています。
(追記)リファクタリング
自分が開発に携わったシステムは紹介させてもらった方法で稼働中です。
ここからはこの記事を作成している中で、やっぱこうしとけばよかったなと思ったことを書かせてください。
(最初からリファクタリングしたほうで記事にしろやって感じですが、ここの過程も記録に残しておきたいので…)
改めてコードを見ていてよくないなと感じたのは、DI登録対象の定義が別のクラスに散らばってしまっている点です。
上記の自動化を導入する前であれば、(コード量は多いにしても)Startup.csだけを見れば何をDIに登録するのかが分かっていましたが、
今回の自動化を導入するとStartup.csとServiceDependencyCollections.csに登録対象の定義が分かれてしまうので、両方のファイルを見ないといけなくなってしまいます。
「何をDIに登録するか」の定義はStartup.csのみに持たせて、拡張メソッドは渡された対象を処理するだけ、というのがベターな設計かなと思いました。
以上を踏まえて、紹介させてもらったコードを次のようにリファクタリングしてみました。
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using System.Reflection;
namespace Hoge.DependencyInjections
{
public static class DependencyInjector
{
public static IServiceCollection AddDependencyCollections(this IServiceCollection services, string[] nameSpaces)
{
foreach (var nameSpace in nameSpaces)
{
var interfaceTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(e => e.IsInterface && e.FullName!.StartsWith(nameSpace));
var classTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(e => e.IsClass && e.FullName!.StartsWith(nameSpace));
foreach (var interfaceType in interfaceTypes)
{
var implementationType = classTypes.FirstOrDefault(c => c.GetInterfaces().Contains(interfaceType));
if (implementationType != null)
{
services.AddScoped(interfaceType, implementationType);
}
}
}
return services;
}
}
}
using Hoge.DependencyInjections;
namespace Hoge
{
public class Startup
{
private readonly string[] serviceDependencyInjectionNameSpaces
= { $"{nameof(Hoge)}.{nameof(Hoge.Services)}",
$"{nameof(Hoge)}.{nameof(Areas)}.{nameof(Areas.Fuga)}.{nameof(Areas.Fuga.Services)}"};
public void ConfigureServices(IServiceCollection services)
{
services.AddDependencyCollections(serviceDependencyInjectionNameSpaces);
services.AddControllersWithViews();
}
}
}