65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (4投票s)

2012 年 10 月 2 日

CPOL

8分钟阅读

viewsIcon

69949

downloadIcon

961

本文旨在演示如何在 MVC 控制器操作中使用拦截器模式,从而可以在控制器类中拦截操作,而无需使用操作筛选器。

引言

在软件系统中,不断增长的需求要求它们执行超出原始规范范围的功能,因此设计能够应对未来多样化需求的系统总是可取的,也是一种好的设计技术。

在本文中,我将演示一种无需使用操作筛选器即可拦截 MVC 控制器操作的机制,以及它在某些场景下的用途,特别是对于遵循可插拔架构和满足多样化软件需求的应用。

众所周知,ASP.NET MVC 中有 操作筛选器,并且从 3.0 版本开始,全球操作筛选器可用。使用操作筛选器,可以处理操作前和操作后的逻辑。此类操作筛选器可以通过在操作方法级别或控制器类级别使用 filter 属性进行实现。

一个经典的例子是使用 HandleErrorAttribute 来处理操作方法抛出的错误。   

[HandleError]
public class HomeController : Controller { }

现在考虑这样一个场景:当一个 XYZ 插件被添加到应用程序中,并且您想将 AccountController 的 LogOn 操作的执行重定向到您的 XYZ 插件的控制器操作,如何在不修改 AccountController 类基本实现的情况下做到这一点?这可以通过使用拦截机制来实现。

我将对此进行描述,但在此之前,有必要对操作筛选器进行一些背景介绍。

背景

有许多筛选属性,如 AuthorizeAttributeOutputCacheAttribute 等。除此之外,您还可以通过继承 ActionFilterAttribute 类在自定义属性类中创建自己的操作筛选器属性。

您可以通过 System.Web.Mvc.GlobalFilterCollection 在全局级别注册这些属性,这样它们就可以被所有操作方法调用。

void Application_Start()
{
    GlobalFilters.Filters.Add(new HandleErrorAttribute());
}

在操作筛选器属性类中,实现了 IActionFilter 接口。通过它,您可以在操作筛选器类的虚拟 OnActionExecutingOnActionExecuted 方法中挂钩操作调用,并在其中分别放置操作前和操作后的逻辑。

在 ASP.NET MVC 中,通过使用筛选器提供程序可以获得应用操作筛选器的额外灵活性。GlobalFilterCollection 只是一个筛选器提供程序,它保存所有全局筛选器的条目,还有两个其他筛选器提供程序,即 FilterAttributeFilterProviderControllerInstanceFilterProvider。同样,您可以拥有自己的自定义筛选器提供程序,通过它可以使用条件筛选来为操作或所有控制器操作应用条件筛选。 

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 方法调用来收集的。

这里有一篇关于筛选器的非常好的文章,这里您可以查看条件筛选器。

从所有这些筛选器描述中,您会了解到可以通过筛选器在调用之前和之后拦截操作调用,但那么拦截机制又是做什么用的呢?

拦截机制

您知道,在设计一个松耦合系统时,总是倾向于考虑未来的可扩展性,当考虑可插拔架构时,这种设计总是非常重要。正如我们讨论的筛选器,它提供了很大的可扩展性,然而,每次引入新筛选器时,我们都需要对基类进行更多更改(例如操作)。

在使用筛选器时,我发现了一些限制(尤其是在模块化开发(松耦合)和可插拔架构中),如下所示:

  • 您无法从一个控制器拦截另一个控制器中的操作调用;要拦截,您需要定义一个筛选属性并将其装饰在操作或控制器上。
  • 您需要在自定义筛选器提供程序中放置一些逻辑来应用这些筛选器。

    您需要使用某种依赖注入机制,通过公开一个合同,在拦截时可以消费该合同。

    在所有这些场景中,您都需要修改控制器和/或筛选器提供程序的现有基类(原始)实现。

  • 您总是需要在 OnActionExecutedOnActionExecuting 方法中放置操作前或操作后的逻辑,没有其他简单的方法可以做到这一点。

为了克服这些限制,一种拦截操作方法调用的简单方法是使用 ASP.NET MVC 控制器中的拦截器模式。

让我们来谈谈拦截器模式的一些基本知识。拦截机制围绕三个基本概念:匹配规则、调用处理程序和拦截器。

  • 匹配规则:简单灵活的对象,用于确定哪些方法应该应用额外的处理。在此,InterceptorsRegistryActionInterceptorAttribute 类被设计用于定义匹配规则。
  • 拦截器调用:主要作用是在任何方法(在本例中是操作)执行之前和之后调度调用到拦截器。为此,创建一个适当的执行上下文,通过该上下文,参数和结果的更改可以在拦截器执行链中共享。在此,BaseControllerInterceptorInvoker 是一个执行点,从这里准备拦截调度程序和执行上下文。InterceptMethodDispatcher 被设计用作调度程序。InterceptorExecutionContext 被设计用于保存执行上下文。 
  • 拦截器:实际挂钩调用的方法。

下面是一篇关于拦截器模式的优秀文章:http://www.edwardcurry.org/web_publications/curry_DEBS_04.pdf

实现

让我们一步一步地理解和实现 MVC 控制器中的拦截机制。

  • 匹配规则:
  • ActionInterceptorAttribute 是一个类似 FilterAttribute 的属性类,区别在于,您必须在需要应用它的操作或控制器之前放置 FilterAttribute。然而,您必须在将作为拦截器方法的方法之前装饰 ActionInterceptorAttribute 属性。这是控制反转的一种体现——“不要给我打电话,我会给你打电话”。在用此属性装饰任何方法后,您需要将该类注册到 InterceptorsRegistry 中。

    定义拦截器的方法有以下规则:

    1. 方法不应返回 ActionResult。这样可以避免拦截的循环行为。
    2. 方法应只有两个参数,即 InterceptorParasDictionary<string,object>object 类型。
    3. 一个类对于一个操作,应只有一个前拦截器方法和一个后拦截器方法。

    通过此属性,有两种方法可以拦截操作。通过使用为此拦截器定义的控制器类型

    [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)

    虽然在 InterceptorsRegistryGetInterceptors 方法中已经放置了获取唯一已注册拦截器的机制。我说,最好只选择一种方式,因为只选择一种方式(无论是通过控制器类型还是视图名称)都可以防止在早期注册阶段出现重复。

    在这里,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 的引用添加到项目中。

    要使用此组件并拦截 HomeControllerIndex 方法,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 框架通过依赖注入进行筛选。
  • 拦截器模式,控制反转。
  • 使用表达式树进行反射。
© . All rights reserved.