原文地址: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); } } }