ASP.NET MVC 控制器依赖注入入门






4.82/5 (128投票s)
在本文中,我将演示一种非常简单明了的方法,通过构造函数将控制器依赖注入到 ASP.NET MVC 框架中。
目录
- 1. 引言
- 2. 为什么要注入控制器依赖?
- 3. 控制器的静态结构
- 4. 简单的自定义控制器
- 5. MVC 框架如何创建控制器?
- 6. 为什么需要自定义控制器工厂?
- 7. 如何创建自定义控制器工厂?
- 8. 使用 MEF 创建自定义控制器工厂
- 9. 值得关注的点
1. 引言
简单来说,如果我想定义 ASP.NET MVC 控制器的职责,那么我可以认为以下是其主要职责:
- 接收 HTTP 请求
- 处理传入的 HTTP 请求
- 处理客户端输入
- 回送响应
- 协调 Model 和 View 之间的关系
ASP.NET MVC 框架本身在运行时创建控制器对象。唯一的先决条件是控制器类必须有一个无参构造函数。但是,如果您需要将对象作为构造函数参数传递,并为此创建了带参数的构造函数,那么会发生什么?简单来说,框架将无法创建那些带有参数化构造函数的控制器对象。在这种情况下,我们需要自己创建控制器对象并将控制器参数注入其中。
您可以通过多种方式将依赖注入到类中。例如:
- Setter 属性
- 方法
- 构造函数
在本文中,我将通过代码示例解释如何使用构造函数将控制器依赖注入到 ASP.NET MVC 框架中。如果不创建自定义控制器工厂,就无法将依赖注入到控制器中。因此,我还将解释如何创建一个非常简单的自定义控制器工厂并将其注册到 ASP.NET MVC 框架。我还将演示一种使用托管可扩展框架 (MEF) 将依赖注入到控制器中的方法。
2. 为什么要注入控制器依赖?
在实际应用程序开发中,您会发现几乎所有的 ASP.NET MVC 应用程序都需要注入其依赖组件。您可以直接在控制器内部创建组件,而不是注入它们。在这种情况下,控制器将与这些组件强耦合。如果某个组件的实现发生更改或该组件发布了新版本,您就必须更改控制器类本身。
当您编写单元测试时,还会遇到另一个问题。您无法独立(隔离地)运行这些控制器的单元测试。您无法从单元测试框架中获得模拟功能。没有模拟,您就无法在隔离环境中运行代码的单元测试。
3. 控制器的静态结构
ASP.NET MVC 框架的控制器结构定义在一个名为 Controller
的抽象类中。如果您想创建一个控制器,首先需要创建一个类,该类必须继承自抽象类 Controller
。控制器类及其层次结构的 UML 类图如下:
所有控制器的根接口是 IController
接口。它的抽象实现是 ControllerBase
类。另一个抽象类继承自 ControllerBase
类,其名称为 Controller
。我们所有的自定义控制器类都应该继承自该 Controller
抽象类或其任何子类。
4. 简单的自定义控制器
如果您创建一个 ASP.NET MVC 项目,您会得到两个默认控制器。一个是 AccountController
,另一个是 HomeController
。
如果查看 HomeController
类的定义,您会发现该类没有构造函数。
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
return View();
}
}
我们都知道,如果没有定义构造函数,.NET Framework 会在编译时创建一个默认的无参构造函数。
public class HomeController : Controller
{
public HomeController()
{
}
}
现在我将创建 ILogger
接口及其实现类 DefaultLogger
。HomeController 类将使用该 ILogger 对象。我将通过构造函数注入它。
public interface ILogger
{
void Log(string logData);
}
public class DefaultLogger : ILogger
{
public void Log(string logData)
{
System.Diagnostics.Debug.WriteLine(logData, "default");
}
}
使用 ILogger
构造函数注入的 HomeController 如下:
public class HomeController : Controller
{
private readonly ILogger _logger;
public HomeController(ILogger logger)
{
_logger = logger;
}
}
我仍然找不到一个地方可以在我的代码库中创建 DefaultLogger
对象,而且由于我不是自己创建 HomeController 对象,所以我找不到创建 DefaultLogger
对象的位置以及如何将其传递给具有已定义参数化构造函数 HomeController
(ILogger logger
) 的 HomeController。在这种状态下,如果我构建项目,它将不会出现任何错误。但在运行时它会抛出异常。错误页面中的异常详情如下:
从上面的堆栈跟踪可以看出,DefaultContollerActivator
类的对象抛出了名为 MissingMethodException
的异常。如果您搜索 MSDN 并查找引发的异常,您会在那里找到它,并且明确提到“当尝试动态访问不存在的方法时引发的异常。”这是内部异常消息。如果看到下一个名为 InvalidOperationException
的异常,它实际上是包装了 MissingMethodException
,在那里您会找到更友好的消息,即“确保控制器有一个无参公共构造函数。”如果我想让 HomeController 可用,我必须添加一个无参构造函数,框架将使用该构造函数创建控制器对象。问题将随之而来:如何将 DefaultLogger
对象传递给该控制器?请保持耐心。
5. MVC 框架如何创建控制器?
在开始描述将 DefaultLogger
对象依赖注入到 HomeController 的过程之前,我们应该清楚一点,那就是 MVC 框架如何创建控制器对象?IControllerFactory
接口负责创建控制器对象。DefaultControllerFactory
是其默认的框架提供的实现类。如果您向 HomeController
类添加一个无参构造函数并在其中设置断点,然后以调试模式运行应用程序,您会发现代码执行会停在那个断点。
如果您查看下面的图片,您可以看到 IControllerFactory
类型的变量 factory 包含 DefaultControllerFactory
对象。DefaultControllerFactory
具有 Create
、GetControllerInstance
、CreateController
等各种方法,它们在创建 HomeController
对象中起着至关重要的作用。您知道 ASP.NET MVC 框架是一个开源项目,因此如果您想了解有关这些方法及其行为的更多详细信息,可以下载其源代码并查看实现。ASP.NET MVC 使用抽象工厂设计模式来创建控制器类。如果您调试代码并使用快速监视查看值,您可以看到 DefaultControllerFactory
被自动创建为 CurrentControllerFactory
(IControllerFactory
)。
6. 为什么需要自定义控制器工厂?
您已经知道,默认控制器工厂使用无参构造函数创建控制器对象。我们也可以通过无参构造函数注入控制器。请看下面的代码:
public class HomeController : Controller
{
private readonly ILogger _logger;
public HomeController():this(new DefaultLogger())
{
}
public HomeController(ILogger logger)
{
_logger = logger;
}
}
我发现许多开发人员对上述依赖注入过程存在误解。这根本不是依赖注入。这实际上清楚地违反了依赖倒置原则。该原则规定“高层模块不应依赖于低层模块,两者都应依赖于抽象。细节应依赖于抽象。”在上面的代码中,HomeController 本身创建了 DefaultLogger 对象。因此,它直接依赖于 ILogger 实现 (DefaultLogger)。如果将来 ILogger 接口出现另一个实现,HomeController 代码本身就需要更改。因此,这被证明不是正确的做法。所以,如果我们想正确地注入依赖,我们需要带参数的构造函数,通过它可以注入我的 ILogger 组件。因此,DefaultControllerFactory
的当前实现不支持此要求。所以我们需要一个新的自定义控制器工厂。
7. 创建自定义控制器工厂
我在这里解释两种创建自定义控制器工厂的方法:
7.1 方法一:
我们可以通过实现 IControllerFactory
接口来创建一个新的自定义控制器工厂。假设我们的新控制器工厂名为 CustomControllerFactory
。那么它的实现如下:
public class CustomControllerFactory : IControllerFactory { public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { ILogger logger = new DefaultLogger(); var controller = new HomeController(logger); return controller; } public System.Web.SessionState.SessionStateBehavior GetControllerSessionBehavior( System.Web.Routing.RequestContext requestContext, string controllerName) { return SessionStateBehavior.Default; } public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) disposable.Dispose(); } }
现在,首先您需要将 CustomControllerFactory
注册到 MVC 框架。我们可以在 Application_Start
事件中完成此操作。
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { RegisterCustomControllerFactory (); } } private void RegisterCustomControllerFactory () { IControllerFactory factory = new CustomControllerFactory(); ControllerBuilder.Current.SetControllerFactory(factory); }
如果您运行应用程序,您会发现我们的无参构造函数 HomeController
没有被调用,取而代之的是参数化的 HomeController(ILogger logger)
构造函数被 MVC 框架调用。哇!您的问题就这样轻松解决了。
您可以使用一种更通用的方式(通过反射)来创建控制器。
public class CustomControllerFactory : IControllerFactory { private readonly string _controllerNamespace; public CustomControllerFactory(string controllerNamespace) { _controllerNamespace = controllerNamespace; } public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { ILogger logger = new DefaultLogger(); Type controllerType = Type.GetType(string.Concat(_controllerNamespace, ".", controllerName, "Controller")); IController controller = Activator.CreateInstance(controllerType, new[] { logger }) as Controller; return controller; } }
首先,您需要从控制器完整名称创建控制器类型对象。然后,您可以使用 Type.GetType
方法在运行时创建控制器对象,并通过反射注入依赖对象。当前的实现代码存在一些问题,它们是:
- 您需要传递控制器命名空间(通过构造函数),因此每个控制器都应该在同一个命名空间下,并且
- 所有控制器都需要一个接受
ILogger
对象的单一参数化构造函数。否则,它将抛出MissingMethodException
。
7.2 方法二
另一种方法是创建自己的控制器工厂。尽管 MVC 框架的 IControllerFactory
的默认实现是 DefaultControllerFactory
,并且它不是一个 sealed
类。因此,您可以继承该类并重写虚拟方法,然后修改/实现您需要的任何内容。一个实现示例可能如下:
public class CustomControllerFactory : DefaultControllerFactory
{
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
ILogger logger = new DefaultLogger();
IController controller = Activator.CreateInstance(controllerType, new[] { logger }) as Controller;
return controller;
}
}
只需创建一个继承自 DefaultControllerFactory
的新 CustomControllerFactory
类,并重写 GetControllerInstance
方法来实现您的自定义逻辑。
8. 使用 MEF 创建自定义控制器工厂
在实际项目中,您会看到专家们在控制器工厂中使用 IOC 容器来创建/获取 Controller
对象。因为如果您尝试动态创建依赖对象和控制器对象,会遇到很多问题。因此,将任何 IOC 容器添加到您的项目中,并在那里注册所有依赖对象并使用它,将是一种更佳的方法。您可以使用各种流行的 IOC 容器,如 Castle Windsor、Unity、NInject、StructureMap 等。我将演示如何在这种情况中使用托管可扩展框架 (MEF)。MEF 不是一个 IOC 容器。它是一个组合层。您也可以称它为可插拔框架,通过它可以让您在运行时插入依赖组件。MEF 可以完成 IOC 能做的几乎所有类型的工作。何时使用 MEF 何时使用 IOC 取决于您的需求和应用程序架构。我的建议是,当您需要在运行时(运行时即插即用)组合您的组件时,您可以使用 MEF;当您通过静态引用创建您的组件及其依赖组件时,您可以使用 IOC。网上有很多文章可以找到更多关于这方面的详细信息/技术文章。我希望您可以对此进行更多研究,并轻松决定在何时使用哪种方法。顺便说一句,MEF 随 .NET 框架 4.0 一起提供,所以主要好处是,没有第三方组件依赖。首先,您需要为您的项目添加 system.ComponentModel.Composition
引用。
然后,您创建自定义控制器工厂。控制器工厂的名称是 MefControllerFactory
。
public class MefControllerFactory : DefaultControllerFactory
{
private readonly CompositionContainer _container;
public MefControllerFactory(CompositionContainer container)
{
_container = container;
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
Lazy<object, object> export = _container.GetExports(controllerType, null, null).FirstOrDefault();
return null == export
? base.GetControllerInstance(requestContext, controllerType)
: (IController)export.Value;
}
public override void ReleaseController(IController controller)
{
((IDisposable)controller).Dispose();
}
}
CompositContainer
对象在这里就像 IOC 容器一样工作。在 GetControllerInstance
方法内部,我从该对象中获取控制器对象。如果找到 null 值,则从默认控制器(基类)对象获取,否则从 CompositContainer
对象获取。创建 MefControllerFactory
类后,我们需要将其注册到 MVC 框架。注册代码在 Application Start 事件中
protected void Application_Start()
{
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
var composition = new CompositionContainer(catalog);
IControllerFactory mefControllerFactory = new MefControllerFactory(composition);
ControllerBuilder.Current.SetControllerFactory(mefControllerFactory);
}
我使用了 MEF 的 InheritedExportAttribute
、ExportAttribute
和 PartCreationPolicyAttribute
来实现 ILogger
接口和 HomeController
。
MEF 框架基于这些属性创建对象。您只需要记住一点,当您使用 MEF 时,您应该用 PartCreationPolicyAttribute
装饰您的控制器,并将控制器的生命周期设置为“每个请求创建对象”策略。[PartCreationPolicy (CreationPolicy.NonShared)]
否则您会收到错误。默认情况下,它将使用 SharedCreation 策略。
9. 值得关注的点
我尝试以一种非常简单明了的方式解释创建控制器工厂并注入其依赖的各种方法。第一和第二种依赖注入方法只是为了让您了解构建和注入控制器及其依赖的过程。您不应该直接将这些方法用于实际应用程序。您可以为您的实际项目选择任何 IOC 或 MEF 框架。您可以对 MEF 进行更多研究并学习 MEF 的用法和最佳实践,之后您就可以在实际项目中使用它了。近期我计划撰写另一篇文章,演示如何使用各种 IOC 容器(如 Windsor、Unity、NInject、StrucutureMap 等)来注入控制器依赖,并进行比较研究。
示例源代码
在我提供的可下载源代码示例中,我使用了 Visual Studio 2012 和 .NET 框架 4.5。任何人都可以下载并进行尝试。