使用过滤器和 Angular JS 进行 MVC 和 API 异常处理
本文将帮助您处理 MVC 平台中的所有类型错误;MVC 视图、API、404、同步/异步调用,均以统一的用户体验集中处理。
引言
在本文中,我将涵盖由 Angular JS 支持的(MVC 和 API)应用程序中的异常处理的整个概念。它尝试集中处理所有类型的异常(自行引发的业务异常、未处理的异常和 404),方式是告知用户错误信息,并为系统管理员记录足够的信息以便进行错误修复和系统维护。
背景
我们都曾饱受在应用程序中注入 Try
/Catch
块之苦,也体会过在各种类中维护、管理和记录信息的痛苦。然而,异常处理必须集中管理,代码实际上应该摆脱所有不必要的 Try
/Catch
块,遍布所有层。基于异常处理块的编程就像使用 GoTo 的意大利面条式代码风格编程!正如我所相信的,一个恰当的设计和不同层之间的消息流不应该基于异常处理块,我们确实应该避免它们。
那么,我们要涵盖哪些类型的错误呢?
情况 | 用户体验 |
---|---|
1. 路由根本不匹配任何定义的路由。 | 页面未找到,URL 将被记录。 |
2. 路由匹配 URL,但 Controller 不正确 | 页面未找到,URL 将被记录。 |
3. 路由匹配 URL,但 Action 不正确 | 页面未找到,URL 将被记录。 |
4. 发生业务异常 | 需要显示一个有意义的消息,无需记录。 |
5. 发生未处理的异常 | 需要显示一个通用的消息,实际异常消息需要被记录。 |
最终结果
最终结果将是
1. 页面未找到
参照上表,对于第一、第二和第三种情况,我们将看到此屏幕
2. 通用消息
如果我们遇到未处理的异常,或者不应向用户显示异常消息的异常
3. 特定消息
如果我们遇到需要将消息发送到 UI 的业务异常
这就是我们真正应该编写代码的方式
public ActionResult GetAllUniversities()
{
throw new UnixBusinessException("The user has exceeded the quota limit, please buy storage first");
}
A. 后端
我将这些情况归类为上述三种用户体验,但有很多情况我们会遇到其中任何一种消息。正如我所提到的,我们不会逐个案例地涵盖它们,但我们会有一个集中的异常处理和日志记录机制,所以我们所有的系统用例都将是 Try
/Catch
和日志记录免费的!
好了,这就是设计,让我们做一些工程上的事情,构建一些东西。
捕获所有异常的一个地方是 Global.asax
中的 Application_Error
(哦,这似乎太高层了),我们将保持这个地方干净,不触碰它,因为它适用于所有高级和关键操作!所以根本不用弄乱 Global.asax。
protected void Application_Error()
{
// Nothing here at all.
}
在 MVC 中,我们还能在哪里捕获异常?是的,在单个控制器中,在 action 后面或在控制器级别,但这不干净,因为你需要为每个控制器这样做,或者你可能为父控制器这样做!?但对于应用程序的其余部分你会怎么做?不,我们也不打算那样做。我们需要更干净的东西。所以也不要弄乱 Controller!
protected override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
}
那么还有什么呢?是的,MVC 过滤器。它是应用程序的唯一位置,我们可以访问 HttpContext
,并且可以轻松地为每个控制器或整个应用程序打开/关闭它。它们就是为此目的而创建的。
所以我将为 MVC 视图创建一个错误过滤器,并将其构造函数注入一个日志记录器。在这个日志记录器后面,我放了 Elmah 来管理我的异常。
public class CustomErrorFilter : HandleErrorAttribute
{
private readonly IUnixLogger logger;
public CustomErrorFilter()
: this(DependencyResolver.Current.GetService<IUnixLogger>())
{
}
public CustomErrorFilter(IUnixLogger logger)
{
this.logger = logger;
}
public override void OnException(ExceptionContext filterContext)
{
//No further processing when the exception is handled or custom errors are not enabled.
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
if (!(filterContext.Exception is UnixBusinessException))
{
//Logging the actual exception
logger.LogException(filterContext.Exception);
//Changing exception type to a generic exception
filterContext.Exception = new Exception
("An error occurred and logged during processing of this application.");
}
base.OnException(filterContext);
}
}
这里有三个非常重要的点,第一,如果异常已处理或自定义错误消息已禁用,我们将不再进行任何漂亮干净的异常处理。第二,如果异常是故意抛出的业务异常,则无需记录。第三,当记录未处理的异常时,我们将用一个通用异常替换该异常,以便关键错误消息不会暴露给外部世界。
注意 *) 我正在为此应用程序使用 Elmah,它隐藏在我的 IUnixLogger
后面。我不会去配置它,因为它非常简单直接,你可以在它的网站上找到它。
最后,我想通过 Shared 文件夹中的 Error.cshtml 向用户显示一条消息。
@model HandleErrorInfo
@{
Layout = null;
}
@Model.Exception.Message
注意 *) 我正在使用 MVC 中的 Area 概念,但我已将此共享视图放在 Area 文件夹之外的普通 Views 文件夹中,以便应用程序中的所有视图都能从中受益。
注意 *) 为了遵循 DRY 原则,我们只有一个用于这些错误页面的视图,它没有任何布局。原因是如果它有任何布局,当我想内联渲染它时,它会包含所有脚本和 HTML 内容,这并非我们所期望的。
那么还有什么呢?是的,将过滤器注册到 MVC 过滤器集合,并开启自定义错误。
filters.Add(new CustomErrorFilter());
<customErrors mode="On" defaultRedirect="~/Error">
</customErrors>
从代码中可以看出,我只是在渲染一个 action 的调用过程中抛出一个业务异常,结果如下,没有任何其他干扰
public ActionResult GetAllUniversities()
{
throw new UnixBusinessException("The user has exceeded the quota limit, please buy storage first");
}
如果异常是系统抛出的未处理异常,例如
public IEnumerable<UniversityViewModel> GetAllUniversities()
{
throw new Exception("Sensitive Info");
}
结果将是
好了,到目前为止我们已经为简单的 MVC 视图实现了集中的业务异常和未处理异常处理机制。那么当我们将 Ajax 调用发送到 Web API 方法时会发生什么?
答案需要更多工作,因为 API 的 Error Attributes 有点不同,因为它们继承自 ExceptionFilterAttribute
。
public class CustomApiErrorFilter : ExceptionFilterAttribute
{
private readonly IUnixLogger logger;
public CustomApiErrorFilter()
: this(DependencyResolver.Current.GetService<IUnixLogger>())
{
}
public CustomApiErrorFilter(IUnixLogger logger)
{
this.logger = logger;
}
public override void OnException(HttpActionExecutedContext filterContext)
{
//Exact message for business exceptions which have been thrown deliberately need to be shown
//However for other types of unhandled exceptions a generic message should be shown.
var exceptionMessage = string.Empty;
if (filterContext.Exception is UnixBusinessException)
{
exceptionMessage = filterContext.Exception.Message;
}
else
{
//Logging Exception
logger.LogException(filterContext.Exception);
//Changing exception message to something generic.
exceptionMessage = "An error occurred and logged during processing of this application.";
}
//Throwing a proper message for the client side
var message = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(exceptionMessage),
ReasonPhrase = exceptionMessage
};
throw new HttpResponseException(message);
}
}
不要惊慌,这和之前的代码一样,除了最后几行。OnException
中的第一部分是为了区分自行抛出的业务异常和未处理的异常。而第二部分基本上是创建一个正确的 HttpResponseException
并将其发送到浏览器。
不要忘记将此属性添加到 Api
属性集合中,该集合的类型为 HttpFilterCollection
。
filters.Add(new CustomApiErrorFilter());
就这样。API 现在得到了支持。太棒了!有趣的部分?用户将获得与普通 MVC 视图渲染和 API 调用完全相同的体验。所有这些都由同一个地方管理。错误过滤器。
B. 前端 (Angular JS)
现在是前端部分。对于前端,我们也需要一个集中的位置来捕获、处理和显示异常。捕获所有请求信息、响应以及它们的错误的最佳位置是什么?服务、工厂、控制器、视图?都不行!!!因为我们需要干净集中的代码来实践 DRY。
那么答案是什么?是的,拦截器。一个在信息被传递给视图和到达 MVC 服务器之前我们可以访问它的地方。
//Registering Interceptors
ngModule.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $rootScope) {
return {
request: function (config) {
//the same config / modified config / a new config needs to be returned.
return config;
},
requestError: function (rejection) {
//Initializing error list
if ($rootScope.errorList == undefined) {
$rootScope.errorList = [];
}
$rootScope.errorList.push(rejection.data);
//It has to return the rejection, simple reject call doesn't work
return $q.reject(rejection);
},
response: function (response) {
//the same response/modified/or a new one need to be returned.
return response;
},
responseError: function (rejection) {
//Initializing the error list
if ($rootScope.errorList == undefined) {
$rootScope.errorList = [];
}
//Adding to error list
$rootScope.errorList.push(rejection.data);
//It has to return the rejection, simple reject call doesn't work
return $q.reject(rejection);
}
};
});
}]);
这里有两个重要的函数:requestError
和 responseError
。所以如果你看代码,这里没有难度,除了这两个函数中的最后一行
return $q.reject(rejection);
这基本上意味着我正在将异常传播到调用堆栈。让我们从一个问题开始:你是否遇到过这样的问题,你的 Ajax 调用总是只调用成功块,尽管你在后端引发了异常?这就是原因。
如果像其他方法一样,我简单地返回拒绝,那么随后的异步调用就不会被告知发生了错误。所以例如,如果我不调用 $q.reject
,$http
调用中的任何错误函数都不会被触发。
var deferred = q.defer();
http({
url: url,
method: 'GET',
params: argument,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).success(function (data) {
deferred.resolve(data);
}).error(function (data, status) {
deferred.reject(data);
});
return deferred.promise;
因此,我们无法通知用户发生了错误。所以拒绝并返回这个拒绝非常重要。
再次参考拦截器,我只是将所有错误收集在 errorList
Angular 模型对象中,然后我可以在我的布局页面(或 SPA 主屏幕)中像下面这样列出它们:
<div class="alert alert-danger"
role="alert" data-ng-repeat="error in errorList">
<p>{{ error }}</p>
</div>
我认为这是 Angular 功能的一个绝佳用途。你觉得呢?
现在是最后一块:404.0 未找到错误
这是棘手的部分之一。MVC 没有一个很好且优雅的方法来处理 404。所以我们需要自己动手。我不会使用自定义 IIS 错误页面,因为它们没有给我足够的灵活性和控制权来处理 404 错误。因此,我将创建自己的 Controller Factory,所以如果我有错误的路由或者正确的路由但控制器/Action 值不正确,我将向浏览器发送 PageNotFound
视图,而不是丑陋的错误消息。
public class UnixControllerFactory : DefaultControllerFactory
{
/// <summary>
/// Instantiates the proper controller, including a right 404 error page
/// </summary>
/// <param name="requestContext"></param>
/// <param name="controllerType"></param>
/// <returns></returns>
protected override IController GetControllerInstance
(RequestContext requestContext, Type controllerType)
{
try
{
//Try to instantiate the controller as usual
var controller = base.GetControllerInstance(requestContext, controllerType);
return controller;
}
//If failed, instantiate 404 and return
catch (HttpException ex)
{
if (ex.GetHttpCode() == 404)
{
MyController errorController = new ErrorController();
errorController.SendHttpNotFound(requestContext.HttpContext);
return errorController;
}
else
{
throw ex;
}
}
}
}
在上面的代码片段中,我实际上正在尝试像往常一样创建一个控制器,失败了?然后创建 404 视图并将其发送到浏览器。
我在我的基控制器中创建了 SendHttpNotFound
,它只是创建一个 404 视图并发送出去。然而,我为 PageNotFound
错误分配了一个完全独立的 View
,这与之前的错误页面不同。
注意 *) 不要忘记在这里或 Error Controller 的特定 Action 中记录不正确的 URL。
注意 *) 这个方法的另一个有趣之处在于,它不是从另一个页面重定向的,就像 IIS 自定义错误页面的方法一样。它是一个原本打算作为 404 处理程序的视图。
关注点
我相信这个方法有趣之处在于,它集中处理了 MVC 和 Web API 的所有可能异常,使用户对两者都有相同的用户体验。而且我处理 404 的方式给了开发人员足够的灵活性来完全处理 404 错误。例如,你也可以将一个具有无效 Object Id 的现有视图作为一个 404 错误来处理,而无需额外的努力,只需在任何控制器 Action 中调用 SendHttpNotFound
。
另一方面,你可以通过将异常继承自你的主业务异常类来决定记录哪些异常,其余的将是自动的。