ASP.NET MVC Web APIプロジェクトで、Unity.WebAPIを使ってConstructor によるDIを実現する手順。
Table of Contents
変更前のコード
ロジックの本体をサービスクラスに出す構成です。ASP.NET MVC Web APIを有効にしたプロジェクトを作成したときにデフォルトでできるコードをちょっとだけ変えたもの。
これだとControllerのAction内でServiceレイヤのインスタンスを作ってしまっているのでUTがやりにくい。ServiceレイヤのインスタンスをDIでControllerに引き渡すのが目標になります。
ControllerのGETメソッド用Action(int配列をServiceレイヤのメソッドで変換し、結果を「number<結果>」文字列の配列に変換して返す)。
// GET api/values public IEnumerable&amp;lt;string&amp;gt; Get() { var numbers = new[] {1, 2}; var service = new ValuesService(); return service.GetDoubled(numbers) .Select(num =&amp;gt; $"number{num}"); }
サービスレイヤのクラス
public class ValuesService { public int[] GetDoubled(int[] input) { return input.Select(num =&amp;gt; num * 2).ToArray(); } }
Unity.WebAPIのNuGetパッケージの導入
DIコンテナとしてUnityを使います。NuGetにWebAPI向けのパッケージがあるのでこれをインストールします。
- ツール>NuGet パッケージマネージャ>ソリューションの NuGet パッケージの管理を開く
- 検索ボックスに「Unity.WebAPI」と入力して検索
- 検索されたUnity.WebAPIを選択し、対象プロジェクトにインストール
Global.asax.csでUnityを登録
以下のハイライトの1行を追加。これを忘れると、ControllerにDefault ConstructorがないことによるArgumentExceptionが発生しました。
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); UnityConfig.RegisterComponents(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
DI注入されるクラスをInterfaceに引き上げ
Dependency Injectionで追加したい(Controllerに外部から注入したい)クラス(ValuesService)をInterfaceに引き上げます(同じ.csファイル内にIValuesServiceインターフェースを作成し、publicメソッドの定義を作成)。
Unity.WebAPIでこのInterfaceが要求されたときは、このクラスのインスタンスを登録してください、という設定を行うためです。
public interface IValuesService { int[] GetDoubled(int[] input); } public class ValuesService { public int[] GetDoubled(int[] input) { return input.Select(num =&amp;gt; num * 2).ToArray(); } }
テスト対象クラスのコンストラクタに引き上げたインターフェースを渡す
Controllerにコンストラクタを追加し、そのパラメータに上で作ったインターフェースを渡します。
渡されたインターフェースはメンバ変数「_service」に格納されます。各Action中ではServiceレイヤをインスタンス化せずに利用できるようになります。ユニットテストクラスでは、このIValuesServiceにモックを渡してテストできます。
private readonly IValuesService _service; public ValuesController(IValuesService service) { _service = service; } // GET api/values public IEnumerable<string> Get() { var numbers = new[] {1, 2}; return _service.GetDoubled(numbers) .Select(num => $"number{num}"); }
UnityConfigでインターフェースに対して解決すべきクラスを登録
Unity.WebAPIをセットアップすると自動で配置されるUnityConfig.csを変更し、IValuesServicesインターフェースが要求されたときに、ValuesServicesクラスのインスタンスを渡すように設定します。
ハイライト部分だけを追加。
public static void RegisterComponents() { var container = new UnityContainer(); // register all your components with the container here // it is NOT necessary to register your controllers // e.g. container.RegisterType<ITestService, TestService>(); container.RegisterType<IValuesService, ValuesService>(); GlobalConfiguration.Configuration.DependencyResolver = new UnityDependencyResolver(container); }
動作確認
デバッグ実行して、「http://localhost:<テスト用ポート番号>/api/Values」にブラウザからアクセス。Controller中で明示的にサービスクラスをインスタンス化しなくても、正しく処理できました。
結果
<ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> <string>number2</string> <string>number4</string> </ArrayOfstring>