ASP.NET MVC 控制器操作中的拦截器模式






4.40/5 (4投票s)
本文旨在演示如何在 MVC 控制器操作中使用拦截器模式,从而可以在控制器类中拦截操作,而无需使用操作筛选器。
引言
在软件系统中,不断增长的需求要求它们执行超出原始规范范围的功能,因此设计能够应对未来多样化需求的系统总是可取的,也是一种好的设计技术。
在本文中,我将演示一种无需使用操作筛选器即可拦截 MVC 控制器操作的机制,以及它在某些场景下的用途,特别是对于遵循可插拔架构和满足多样化软件需求的应用。
众所周知,ASP.NET MVC 中有 操作筛选器,并且从 3.0 版本开始,全球操作筛选器可用。使用操作筛选器,可以处理操作前和操作后的逻辑。此类操作筛选器可以通过在操作方法级别或控制器类级别使用 filter
属性进行实现。
一个经典的例子是使用 HandleErrorAttribute
来处理操作方法抛出的错误。
[HandleError]
public class HomeController : Controller { }
现在考虑这样一个场景:当一个 XYZ 插件被添加到应用程序中,并且您想将 AccountController
的 LogOn 操作的执行重定向到您的 XYZ 插件的控制器操作,如何在不修改 AccountController
类基本实现的情况下做到这一点?这可以通过使用拦截机制来实现。
我将对此进行描述,但在此之前,有必要对操作筛选器进行一些背景介绍。
背景
有许多筛选属性,如 AuthorizeAttribute
、OutputCacheAttribute
等。除此之外,您还可以通过继承 ActionFilterAttribute
类在自定义属性类中创建自己的操作筛选器属性。
您可以通过 System.Web.Mvc.GlobalFilterCollection
在全局级别注册这些属性,这样它们就可以被所有操作方法调用。
void Application_Start()
{
GlobalFilters.Filters.Add(new HandleErrorAttribute());
}
在操作筛选器属性类中,实现了 IActionFilter
接口。通过它,您可以在操作筛选器类的虚拟 OnActionExecuting
和 OnActionExecuted
方法中挂钩操作调用,并在其中分别放置操作前和操作后的逻辑。
在 ASP.NET MVC 中,通过使用筛选器提供程序可以获得应用操作筛选器的额外灵活性。GlobalFilterCollection
只是一个筛选器提供程序,它保存所有全局筛选器的条目,还有两个其他筛选器提供程序,即 FilterAttributeFilterProvider
和 ControllerInstanceFilterProvider
。同样,您可以拥有自己的自定义筛选器提供程序,通过它可以使用条件筛选来为操作或所有控制器操作应用条件筛选。
public class ConditionalFilterProvider : IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext,
ActionDescriptor actionDescriptor)
{
//place here your logic for application of conditional filters.
}
}
然后可以将此提供程序注册为
var provider = new ConditionalFilterProvider();
FilterProviders.Providers.Add(provider);
您也可以删除现有提供程序。
var oldProvider = FilterProviders.Providers.Single(
f => f is FilterAttributeFilterProvider
);
FilterProviders.Providers.Remove(oldProvider);
这些筛选器是通过 ControllerActionInvoker
中的 FilterProviders.Providers.GetFilters
方法调用来收集的。
这里有一篇关于筛选器的非常好的文章,这里您可以查看条件筛选器。
从所有这些筛选器描述中,您会了解到可以通过筛选器在调用之前和之后拦截操作调用,但那么拦截机制又是做什么用的呢?
拦截机制
您知道,在设计一个松耦合系统时,总是倾向于考虑未来的可扩展性,当考虑可插拔架构时,这种设计总是非常重要。正如我们讨论的筛选器,它提供了很大的可扩展性,然而,每次引入新筛选器时,我们都需要对基类进行更多更改(例如操作)。
在使用筛选器时,我发现了一些限制(尤其是在模块化开发(松耦合)和可插拔架构中),如下所示:
- 您无法从一个控制器拦截另一个控制器中的操作调用;要拦截,您需要定义一个筛选属性并将其装饰在操作或控制器上。
- 您总是需要在
OnActionExecuted
或OnActionExecuting
方法中放置操作前或操作后的逻辑,没有其他简单的方法可以做到这一点。
或
您需要在自定义筛选器提供程序中放置一些逻辑来应用这些筛选器。
或
您需要使用某种依赖注入机制,通过公开一个合同,在拦截时可以消费该合同。
在所有这些场景中,您都需要修改控制器和/或筛选器提供程序的现有基类(原始)实现。
为了克服这些限制,一种拦截操作方法调用的简单方法是使用 ASP.NET MVC 控制器中的拦截器模式。
让我们来谈谈拦截器模式的一些基本知识。拦截机制围绕三个基本概念:匹配规则、调用处理程序和拦截器。
- 匹配规则:简单灵活的对象,用于确定哪些方法应该应用额外的处理。在此,
InterceptorsRegistry
和ActionInterceptorAttribute
类被设计用于定义匹配规则。 - 拦截器调用:主要作用是在任何方法(在本例中是操作)执行之前和之后调度调用到拦截器。为此,创建一个适当的执行上下文,通过该上下文,参数和结果的更改可以在拦截器执行链中共享。在此,
BaseControllerInterceptorInvoker
是一个执行点,从这里准备拦截调度程序和执行上下文。InterceptMethodDispatcher
被设计用作调度程序。InterceptorExecutionContext
被设计用于保存执行上下文。 - 拦截器:实际挂钩调用的方法。
下面是一篇关于拦截器模式的优秀文章:http://www.edwardcurry.org/web_publications/curry_DEBS_04.pdf。
实现
让我们一步一步地理解和实现 MVC 控制器中的拦截机制。
- 匹配规则:
- 方法不应返回
ActionResult
。这样可以避免拦截的循环行为。 - 方法应只有两个参数,即
InterceptorParasDictionary<string,object>
和object
类型。 - 一个类对于一个操作,应只有一个前拦截器方法和一个后拦截器方法。
ActionInterceptorAttribute
是一个类似 FilterAttribute
的属性类,区别在于,您必须在需要应用它的操作或控制器之前放置 FilterAttribute
。然而,您必须在将作为拦截器方法的方法之前装饰 ActionInterceptorAttribute
属性。这是控制反转的一种体现——“不要给我打电话,我会给你打电话”。在用此属性装饰任何方法后,您需要将该类注册到 InterceptorsRegistry
中。
定义拦截器的方法有以下规则:
通过此属性,有两种方法可以拦截操作。通过使用为此拦截器定义的控制器类型
[ActionInterceptor(InterceptionOrder.Before, typeof(AccountController), "LogOn")]
public void SubmitLogOn(InterceptorParasDictionary<string, object> paras, object result)
以及通过使用系统中唯一的视图名称或模型名称
[ActionInterceptor(InterceptionOrder.After, "Account", "LogOn")]
public object MyLogOn(InterceptorParasDictionary<string, object> paras, object result)
虽然在 InterceptorsRegistry
的 GetInterceptors
方法中已经放置了获取唯一已注册拦截器的机制。我说,最好只选择一种方式,因为只选择一种方式(无论是通过控制器类型还是视图名称)都可以防止在早期注册阶段出现重复。
在这里,InterceptionOrder
是一个枚举,用于指定拦截器的执行顺序,即操作之前或之后。如果未指定,则默认为 *之后*。
在 ActionInterceptorAttribute
中,有一个可选参数 breakExecutionOnException
,如果设置为 true(默认始终为 true),则在发生异常时,它将从该点终止执行,并且所有后续的拦截器执行链(包括操作)都将被终止。
ActionInterceptor
类用于保存拦截器信息,同时用于在 InterceptorsRegistry
中注册。
这是类图,MVC 控制器的基本原理是,您可以通过 IActionInvoker
拦截任何操作,这就是为什么新的 BaseControllerInterceptorInvoker
类派生自基类 ControllerActionInvoker
。
在此调用器中,重写了 InvokeActionMethod
以拦截操作。对于将要包含拦截器的类,方法必须派生自 BaseMvcController
,或者实现 IInterceptorMvcController
,并且控制器类应派生自 BaseMvcController
。
[ActionInterceptor(InterceptionOrder.Before, "Account", "LogOn")]
public void SubmitLogOn(InterceptorParasDictionary<string, object> paras, object result)
{
//some logic
this.InterceptorExecutionContext.CancelAllExecutions = true;
}
创建 ASP.NET MVC 3 Web 应用程序后,将 MvcCallInterceptors 的引用添加到项目中。
要使用此组件并拦截 HomeController
的 Index
方法,HomeController
的基类设置为 BaseMvcController
,并重写 View
属性以指定此控制器服务的模型或视图。
protected override string View
{
get { return "Home"; }
}
一个新的类 MyHomeAccountController
派生自 BaseMvcController
,下面为 HomeController Index
方法添加了拦截器。
public class MyHomeAccountController : BaseMvcController
{
[ActionInterceptor("Home", "Index")]
public object Index(InterceptorParasDictionary<string,object> paras, object result)
{
(result as ViewResult).ViewBag.Message =
(result as ViewResult).ViewBag.Message + " Hey, You have been intercepted.";
return result;
}
}
最后一步是注册 MyHomeAccountController
类。
MvcCallInterceptors.Interceptors.InterceptorsRegistry.RegisterInterceptors<MyHomeAccountController>();
同样,在同一个 MyHomeAccountController
类中,为 AccountController LogOn
操作创建了两个拦截器。
//Before : change in parameter value.
[ActionInterceptor(InterceptionOrder.Before, "Account", "LogOn")]
public void SubmitLogOn(InterceptorParasDictionary<string, object> paras, object result)
{
if (paras.Count > 0)
{
(paras["model"] as LogOnModel).UserName = "***" + (paras["model"] as LogOnModel).UserName;
}
}
//After : Redirected to different view
[ActionInterceptor(InterceptionOrder.After, "Account", "LogOn")]
public object MyLogOn(InterceptorParasDictionary<string, object> paras, object result)
{
if (paras.Count == 0)
{
return View("MyLogOn");
}
else
{
return result;
}
}
通过拦截机制,为了验证筛选器是否按正常方式执行,在 MyHomeAccountController Index
方法中注释掉了代码,取消注释并检查调用是否已传递到 HandleErrorAttribute
筛选器以进行自定义错误处理。
除了拦截器模式的优点之外,还有一个缺点,拦截器模式增加了设计的复杂性。越多的拦截器可以挂钩到系统,其接口就越臃肿。该模式固有的开放性也会给系统带来潜在的漏洞。通过这种开放的设计,可能会引入恶意的拦截器或仅仅是错误的拦截器,从而导致系统损坏或错误。
我有一些关于增强此机制的有趣想法。
- 我们可以在注册任何类的过程中在
InteceptorsRegistry
类中放置一个锁,以实现线程安全,这在 Web 应用程序中是必要的。 - 绕过拦截器类的重复注册。
- 在拦截器方法中提供依赖注入机制。(这只是一个想法。)
- 通过设计一个控制器级别的属性来拦截控制器中的所有方法。
我将在下一版文章中尝试实现上述内容。
关注点
我学习并有机会更深入地研究了一些以下有趣的方面:
- ASP.NET MVC 框架。
- 使用 MVC Unity 框架通过依赖注入进行筛选。
- 拦截器模式,控制反转。
- 使用表达式树进行反射。