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

使用过滤器和 Angular JS 进行 MVC 和 API 异常处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (9投票s)

2014 年 12 月 30 日

CPOL

8分钟阅读

viewsIcon

59422

本文将帮助您处理 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);
            }
        };
    });
}]);

这里有两个重要的函数:requestErrorresponseError。所以如果你看代码,这里没有难度,除了这两个函数中的最后一行

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

另一方面,你可以通过将异常继承自你的主业务异常类来决定记录哪些异常,其余的将是自动的。

© . All rights reserved.