摘自小牛之路

Exception Filter,在下面三种来源抛出未处理的异常时运行:

  • 另外一种Filter(如Authorization、Action或Result等Filter)。
  • Action方法本身。
  • Action方法执行完成(即处理ActionResult的时候)。

Exception Filter必须实现 IExceptionFilter 接口,该接口的定义如下:

namespace System.Web.Mvc { 
    public interface IExceptionFilter { 
        void OnException(ExceptionContext filterContext); 
    } 
} 

ExceptionContext 常用属性说明

在 IExceptionFilter 的接口定义中,唯一的 OnException 方法在未处理的异常引发时执行,其中参数的类型:ExceptionContext,它继承自 ControllerContext 类,ControllerContext 包含如下常用的属性:

  • Controller,返回当前请求的controller对象。
  • HttpContext,提供请求和响应的详细信息。
  • IsChildAction,如果是子action则返回true(稍后将简单介绍子action)。
  • RequestContext,提供请求上下文信息。
  • RouteData,当前请求的路由实例信息。

作为继承 ControllerContext 类的子类,ExceptionContext 类还提供了以下对处理异常的常用属性:

  • ActionDescriptor,提供action方法的详细信息。
  • Result,是一个 ActionResult 类型,通过把这个属性值设为非空可以让某个Filter的执行取消。
  • Exception,未处理异常信息。
  • ExceptionHandled,如果另外一个Filter把这个异常标记为已处理则返回true。

一个Exception Filter可以通过把 ExceptionHandled 属性设置为true来标注该异常已被处理过,这个属性一般在某个action方法上应用了多个Exception Filter时会用到。ExceptionHandled 属性设置为true后,就可以通过该属性的值来判断其它应用在同一个action方法Exception Filter是否已经处理了这个异常,以免同一个异常在不同的Filter中重复被处理。

 

示例演示

在 Infrastructure 文件夹下添加一个 RangeExceptionAttribute.cs 类文件,代码如下:

public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter {
    public void OnException(ExceptionContext filterContext) {
        if (!filterContext.ExceptionHandled && filterContext.Exception is ArgumentOutOfRangeException) {
            filterContext.Result = new RedirectResult("~/Content/RangeErrorPage.html");
            filterContext.ExceptionHandled = true;
        }
    }
}

这个Exception Filter通过重定向到Content目录下的一个静态html文件来显示友好的 ArgumentOutOfRangeException 异常信息。我们定义的 RangeExceptionAttribute 类继承了FilterAttribute类,并且实现了IException接口。作为一个MVC Filter,它的类必须实现IMvcFilter接口,你可以直接实现这个接口,但更简单的方法是继承 FilterAttribute 基类,该基类实现了一些必要的接口并提供了一些有用的基本特性,比如按照默认的顺序来处理Filter。

在Content文件夹下面添加一个名为RangeErrorPage.html的文件用来显示友好的错误信息。如下所示:

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
    <title>Range Error</title> 
</head> 
<body> 
    <h2>Sorry</h2> 
    <span>One of the arguments was out of the expected range.</span> 
</body> 
</html>

在HomeController中添加一个值越限时抛出异常的action,如下所示:

    public class HomeController : Controller { 
        [RangeException]
        public string RangeTest(int id) { 
            if (id > 100) { 
                return String.Format("The id value is: {0}", id); 
            } else { 
                throw new ArgumentOutOfRangeException("id", id, ""); 
            } 
        } 
    } 

当对RangeTest应用自定义的的Exception Filter时,运行程序URL请求为 /Home/RangeTest/50,程序抛出异常后将重定向到RangeErrorPage.html页面:

 

由于静态的html文件是和后台脱离的,所以实际项目中更多的是用一个View来呈现友好的错误信息,以便很好的对它进行一些动态的控制。如下面把示例改动一下,RangeExceptionAttribute 类修改如下:

    public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter {
        public void OnException(ExceptionContext filterContext) {
            if (!filterContext.ExceptionHandled && filterContext.Exception is ArgumentOutOfRangeException) {
                int val = (int)(((ArgumentOutOfRangeException)filterContext.Exception).ActualValue);
                filterContext.Result = new ViewResult {
                    ViewName = "RangeError",
                    ViewData = new ViewDataDictionary<int>(val)
                };
                filterContext.ExceptionHandled = true;
            }
        }
    }

我们创建一个ViewResult对象,指定了发生异常时要重定向的View名称和传递的model对象。然后我们在Views/Shared文件夹下添加一个RangeError.cshtml文件,代码如下:

@model int

<!DOCTYPE html> 
<html> 
<head> 
    <meta name="viewport" content="width=device-width" /> 
    <title>Range Error</title> 
</head> 
<body> 
    <h2>Sorry</h2> 
    <span>The value @Model was out of the expected range.</span> 
    <div> 
        @Html.ActionLink("Change value and try again", "Index") 
    </div> 
</body> 
</html> 

运行结果如下:

 

禁用异常跟踪

很多时候异常是不可预料的,在每个Action方法或Controller上应用Exception Filter是不现实的。而且如果异常出现在View中也无法应用Filter。如RangeError.cshtml这个View加入下面代码:

@model int

@{ 
    var count = 0; 
    var number = Model / count; 
} 

...

运行程序后,将会显示如下信息:

 

然程序发布后不应该显示这些信息给用户看。我们可以通过配置Web.config让应用程序不管在何时何地引发了异常都可以显示统一的友好错误信息。在Web.config文件中的<system.web>节点下添加如下子节点:

  <system.web>
    
    ...
    <customErrors mode="On" defaultRedirect="/Content/RangeErrorPage.html"/>
  </system.web>

这个配置只对远程访问有效,本地运行站点依然会显示跟踪信息。

 

 

使用内置的 Exceptin Filter

通过上面的演示,我们理解了Exceptin Filter在MVC背后是如何运行的。但我们并不会经常去创建自己的Exceptin Filter,因为微软在MVC框架中内置的 HandleErrorAttribute(实现了IExceptionFilter接口) 已经足够我们平时使用。它包含ExceptionType、View和Master三个属性。当ExceptionType属性指定类型的异常被引发时,这个Filter将用View属性指定的View(使用默认的Layout或Mast属性指定的Layout)来呈现一个页面。如下面代码所示:

... 
[HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "RangeError")] 
public string RangeTest(int id) { 
    if (id > 100) { 
        return String.Format("The id value is: {0}", id); 
    } else { 
        throw new ArgumentOutOfRangeException("id", id, ""); 
    } 
} 
... 

使用内置的HandleErrorAttribute,将异常信息呈现到View时,这个特性同时会传递一个HandleErrorInfo对象作为View的model。HandleErrorInfo类包含ActionName、ControllerName和Exception属性,如下面的 RangeError.cshtml 使用这个model来呈现信息:

@model HandleErrorInfo 
@{ 
    ViewBag.Title = "Sorry, there was a problem!"; 
} 
 
<!DOCTYPE html> 
<html> 
<head> 
    <meta name="viewport" content="width=device-width" /> 
    <title>Range Error</title> 
</head> 
<body> 
    <h2>Sorry</h2> 
    <span>The value @(((ArgumentOutOfRangeException)Model.Exception).ActualValue) 
        was out of the expected range.</span>         
    <div> 
        @Html.ActionLink("Change value and try again", "Index") 
    </div> 
    <div style="display: none"> 
        @Model.Exception.StackTrace 
    </div> 
</body> 
</html>