原文地址:http://www.remondo.net/using-autofac-resolve-dependencies-mvc-controller/
前言:特翻譯此文,爲親近中文的朋友。
Breaking dependencies in our code is all about enhanced maintainability, flexibility and testability. In the previous
post I created some nasty dependencies in my MVC application. The HomeController instantiates a repository. This repository calls
upon an external system for data in turn. In this case Windows Azure Table Storage. Unit testing becomes cumbersome (even unreliable) with a dependency on any external system holding state. As Jeremy Miller wrote a nice blogpost on the Qualities
of a Good Unit Test in which he says:
我們的代碼中打破(抽離)依賴無非是爲了增強可維護性、可測試性和靈活性。在前面的帖子我在MVC應用程序創建了一些嚴重的依賴性。HomeController實例化一個存儲庫。這個存儲庫調用外部系統(來獲取)數據。其情形是位於Windows Azure的存儲表。過於依賴於外部系統,單元測試將變得冗長繁瑣(甚至不可靠)。Jeremy Miller 關於一個好的單元測試的質量寫了一篇不錯的博客中,他說:
“[..] put evil stateful things behind nice, deterministic mocks or stubs.”
But before we can do that, we need to refactor out all these dependencies first. We do this by placing small interfaces between dependent objects. Then we configure an IoC container to take care of injecting the proper dependencies whenever needed. In this example we use Autofac, but it could be any of the well established .NET IoC containers out there.
但是在我們能做到這一點之前,我們首先需要重構出所有這些依賴項。爲此,我們在這些依賴對象之間創建了一些小接口。然後我們配置一個IoC容器,負責注入適當的依賴關係以便於在需要的時候用到。在這個示例中,我們使用Autofac,但它也可以是任何其他的.NET的loc容器。
Clearly state dependencies in the constructor
在構造函數中明確聲明依賴
In this example we have a simple MVC 4 application listing a couple of Olympic gold medal winners. These are retrieved from a SQL database with the Entity Framework. The data retrieval process is encapsulated in a repository. But by newing up the GoldMedalWinnersRepository in the HomeController we have created a permanent and hidden dependency.
在這個例子中,我們有一個簡單的MVC 4應用程序用來列舉出奧運金牌的得主。這些都從一個SQL數據庫用Entity Framework檢索到的數據。數據檢索過程封裝在一個存儲庫中。但是在HomeController中通過新建一個GoldMedalWinnersRepository實例, 我們已經創建了一個永久的隱藏的依賴。
using System.Web.Mvc; using TestableMvcApp.DataContext; using TestableMvcApp.Models; using TestableMvcApp.Repositories; namespace TestableMvcApp.Controllers { public class HomeController : Controller { public ActionResult Index(GoldMedalWinnersModel model) { ViewBag.Message = "Gold Medal Winners"; var repository = new GoldMedalWinnersRepository( new GoldMedalWinnersContext()); model.Winners = repository.GetAll(); return View(model); } } }
Testing this controller forces us to use/test the repository logic as well. Let’s solve this first. To break the dependency with external classes a constructor should ask for those dependencies. So we could move the instantiation of the repository outside the HomeController by passing it in the constructor:
測試該控制器迫使我們使用/測試存儲庫邏輯。讓我們先解決這個依賴。爲了打破與外部類之間的依賴,可以讓構造函數應該要求這些依賴項(作爲參數傳入)。所以我們可以從HomeController中移除存儲庫的實例化,而改用構造函數傳入參數:
using System.Web.Mvc; using TestableMvcApp.Models; using TestableMvcApp.Repositories; namespace TestableMvcApp.Controllers { public class HomeController : Controller { private readonly GoldMedalWinnersRepository _repository; public HomeController(GoldMedalWinnersRepository repository) { _repository = repository; } public ActionResult Index(GoldMedalWinnersModel model) { ViewBag.Message = "Gold Medal Winners"; model.Winners = _repository.GetAll(); return View(model); } } }
It is one small step in the right direction.
這是往正確方向的一小步。
Call upon abstractions
調用抽象(接口/抽象類)
Ok, now the constructor clearly states all of its dependencies. But if we test the controller we still have to deal with the repository calling the database. In order to test the controller independently from the repository we need to pass in a fake version of this repository. This way we have total control of the inner workings of the object while testing the real controller. To do this we need to create an abstraction of the repository; here’s the interface:
好的,現在構造函數明確闡述了它的所有依賴項。但是如果我們測試控制器我們仍然需要處理存儲庫調用數據庫的(邏輯)。爲了使控制器的測試獨立於存儲庫(邏輯),我們需要傳遞一個虛擬存儲庫版本。這種方式我們完全控制對象的內部運作,而測試真正的控制器。要做到這一點,我們需要創建一個存儲庫的抽象;下面是接口:
using System.Collections.Generic; using TestableMvcApp.Pocos; namespace TestableMvcApp.Repositories { public interface IGoldMedalWinnersRepository { GoldMedalWinner GetById(int id); IEnumerable<GoldMedalWinner> GetAll(); void Add(GoldMedalWinner goldMedalWinner); } }
Let’s change the controllers constructor to ask for an abstract IGoldMedalWinnersRepository interface instead of an actual GoldMedalWinnersRepository object.
讓我們改變控制器的構造函數要求傳入一個抽象IGoldMedalWinnersRepository接口而不是實際GoldMedalWinnersRepository對象。
using System.Web.Mvc; using TestableMvcApp.Models; using TestableMvcApp.Repositories; namespace TestableMvcApp.Controllers { public class HomeController : Controller { private readonly IGoldMedalWinnersRepository _repository; public HomeController(IGoldMedalWinnersRepository repository) { _repository = repository; } public ActionResult Index(GoldMedalWinnersModel model) { ViewBag.Message = "Gold Medal Winners"; model.Winners = _repository.GetAll(); return View(model); } } }
using System.Data.Entity; using TestableMvcApp.Pocos; namespace TestableMvcApp.DataContext { public interface IGoldMedalWinnersContext { DbSet<GoldMedalWinner> GoldMedalWinners { get; set; } DbSet<Country> Countries { get; set; } int SaveChanges(); } }Now let the constructor of our repository ask for an implementation of this interface, and we’re nicely decoupled.
using System.Collections.Generic; using System.Linq; using TestableMvcApp.DataContext; using TestableMvcApp.Pocos; namespace TestableMvcApp.Repositories { public class GoldMedalWinnersRepository : IGoldMedalWinnersRepository { private readonly IGoldMedalWinnersContext _goldContext; public GoldMedalWinnersRepository(IGoldMedalWinnersContext goldContext) { _goldContext = goldContext; } #region IGoldMedalWinnersRepository Members public GoldMedalWinner GetById(int id) { return _goldContext.GoldMedalWinners.Find(id); } public IEnumerable<GoldMedalWinner> GetAll() { return _goldContext.GoldMedalWinners.ToList(); } public void Add(GoldMedalWinner goldMedalWinner) { _goldContext.GoldMedalWinners.Add(goldMedalWinner); _goldContext.SaveChanges(); } #endregion } }At this point we can pass any repository that implements our interface. So it must be quite easy to fake, stub or mock the repository and dbcontext within our unit tests.
Using an IoC container to resolve dependencies
使用IoC容易來解析依賴
using System.Web.Mvc; using Autofac; using Autofac.Integration.Mvc; using TestableMvcApp.DataContext; using TestableMvcApp.Repositories; namespace TestableMvcApp.App_Start { public class IocConfig { public static void RegisterDependencies() { var builder = new ContainerBuilder(); builder.RegisterControllers(typeof (MvcApplication).Assembly); builder.RegisterType<GoldMedalWinnersRepository>() .As<IGoldMedalWinnersRepository>() .InstancePerHttpRequest(); builder.RegisterType<GoldMedalWinnersContext>() .As<IGoldMedalWinnersContext>() .InstancePerHttpRequest(); IContainer container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); } } }We new up an Autofac ContainerBuilder and call RegisterControllers for the MVC assembly. This method registers all controllers in the application with the AutofacDependencyResolver. Whenever MVC asks for a controller this resolver return the proper controller.
using System.Web; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using TestableMvcApp.App_Start; namespace TestableMvcApp { public class MvcApplication : HttpApplication { protected void Application_Start() { IocConfig.RegisterDependencies(); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } } }