ASP.NET MVC下基于异常处理的完整解决方案

EntLib的异常处理应用块(Exception Handling Application Block)是一个不错的异常处理框架,它使我们可以采用配置的方式来定义异常处理策略。而ASP.NET MVC是一个极具可扩展开发框架,在这篇文章中我将通过它的扩展实现与EntLib的集成,并提供一个完整的解决异常处理解决方案。[源代码从这里下载]

目录 一、基本异常处理策略 二、通过自定义Action处理异常 三、通过配置的Error View处理异常 四、自定义ActionInvoker:ExceptionActionInvoker 五、自定义Controller:BaseController

一、基本异常处理策略

我们首先来讨论我们的解决方案具体采用的异常处理策略:

  • 对于执行Controller的某个Action方法抛出的异常,我们会按照指定配置策略进行处理。我们可以采取日志记录、异常替换和封装这些常用的异常处理方式;
  • 对于处理后的异常,如果异常处理策略规定需要将其抛出,则会自动重定向到与异常类型匹配的出错页面。我们会维护一个异常类型和Error View的匹配关系;
  • 对于处理后的异常,如果异常处理策略规定不需要将其抛出,则会执行与当前Action操作相匹配的错误处理Action进行处理。异常处理Action方法默认采用“On{Action}Error”这样的命名规则,而当前上下文会与异常处理操作方法的参数进行绑定。除次之外,我们会设置当前ModelState的错误信息;
  • 如果用户不曾定义相应的异常处理Action,依然采用“错误页面重定向”方式进行异常处理。

二、通过自定义Action处理异常

为了让读者对上面介绍的异常处理页面有一个深刻的理解,我们来进行一个实例演示。该实例用于模拟用户登录,我们定义了如下一个只包含用户名和密码两个属性的Model:LoginInfoModel。

   1: namespace Artech.Mvc.ExceptionHandling.Models
   2: {
   3:     public class LoginInfo
   4:     {
   5:         [Display(Name ="User Name")]
   6:         [Required(ErrorMessage = "User Name is manadatory!")]
   7:         public string UserName { get; set; }
   8:  
   9:         [Display(Name = "Password")]
  10:         [DataType(DataType.Password)]
  11:         [Required(ErrorMessage = "Password is manadatory!")]
  12:         public string Password { get; set; }
  13:     }
  14: }

我们定义了如下一个AccountController,它是我们自定义的BaseController的子类。AccountController在构造的时候调用基类构造函数指定的参数代表异常处理策略的配置名称。SignIn方法代表用于进行“登录”的操作,而OnSignInError就表示该操作对应的异常处理操作。如果在SignIn操作中抛出的异常经过处理后无需再抛出,则会通过调用OnSignInError,而此时ModelState已经被设置了相应的错误消息。

   1: public class AccountController : BaseController
   2: {
   3:     public AccountController()
   4:         : base("myPolicy")
   5:     { }
   6:  
   7:     public ActionResult SignIn()
   8:     {
   9:         return View(new LoginInfo());
  10:     }
  11:     [HttpPost]
  12:     public ActionResult SignIn(LoginInfo loginInfo)
  13:     {
  14:         if (!ModelState.IsValid)
  15:         {
  16:             return this.View(new LoginInfo { UserName = loginInfo.UserName });
  17:         }
  18:  
  19:         if (loginInfo.UserName != "Foo")
  20:         {
  21:             throw new InvalidUserNameException();
  22:         }
  23:  
  24:         if (loginInfo.Password != "password")
  25:         {
  26:             throw new UserNamePasswordNotMatchException();
  27:         }
  28:  
  29:         ViewBag.Message = "Authentication Succeeds!";
  30:         return this.View(new LoginInfo { UserName = loginInfo.UserName });
  31:     }
  32:  
  33:     public ActionResult OnSignInError(string userName)
  34:     {
  35:         return this.View(new LoginInfo { UserName = userName });
  36:     }
  37: }

具体定义在SignIn操作方法中的认证逻辑是这样的:如果用户名不是“Foo”则抛出InvalidUserNameException异常;如果密码不是“password”则抛出UserNamePasswordNotMatchException异常。下面是SignIn操作对应的View的定义:

   1: @model Artech.Mvc.ExceptionHandling.Models.LoginInfo
   2: @{
   3:     ViewBag.Title = "SignIn";
   4: }
   5: @Html.ValidationSummary()
   6: @if (ViewBag.Messages != null)
   7: { 
   8:     @ViewBag.Messages
   9: }
  10: @using (Html.BeginForm())
  11: { 
  12:     @Html.EditorForModel()
  13:     <input type="submit" value="SignIn" />
  14: }

在AccountController初始化时指定的异常处理策略“myPolicy”定义在如下的配置中。我们专门针对SignIn操作方法抛出的InvalidUserNameException和UserNamePasswordNotMatchException进行了处理,而ErrorMessageSettingHandler是我们自定义的异常处理器,它仅仅用于设置错误消息。如下面的代码片断所示,如果上述的这两种类型的异常被抛出,最终的错误消息会被指定为“User name does not exist!”和“User name does not match password!”。

   1: <exceptionHandling>
   2:   <exceptionPolicies>
   3:     <add name="myPolicy">
   4:       <exceptionTypes>
   5:         <add name="InvalidUserNameException" 
   6:                type="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling"
   7:              postHandlingAction="None">
   8:           <exceptionHandlers>
   9:             <add name="ErrorMessageSettingHandler"
  10:                  type="Artech.Mvc.ExceptionHandling.ErrorMessageSettingHandler, Artech.Mvc.ExceptionHandling"
  11:                  errorMessage="User name does not exist!"/>
  12:           </exceptionHandlers>
  13:         </add>
  14:         <add name="UserNamePasswordNotMatchException" 
  15:                type="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling"
  16:              postHandlingAction="None">
  17:           <exceptionHandlers>
  18:             <add name="ErrorMessageSettingHandler"
  19:                  type="Artech.Mvc.ExceptionHandling.ErrorMessageSettingHandler, Artech.Mvc.ExceptionHandling"
  20:                  errorMessage="User name does not match password!"/>
  21:           </exceptionHandlers>
  22:         </add>          
  23:       </exceptionTypes>
  24:     </add>
  25:   </exceptionPolicies>
  26: </exceptionHandling>

现在我们通过路由映射将AccountController和Sign设置为默认Controller和Action后,开启我们的应用程序。在输入错误的用户名和错误明码的情况下在ValidationSummary中将自动得到相应的错误消息。

三、通过配置的Error View处理异常

在上面的配置中,针对InvalidUserNameException和UserNamePasswordNotMatchException这两种异常类型的配置策略都将PostHandlingAction属性设置为“None”,意味着不会将原来的异常和处理后的异常进行重新抛出。现在我们将该属性设置为“ThrowNewException”,意味着我们会将处理后的异常重新抛出来。

   1: <exceptionHandling>
   2:   <exceptionPolicies>
   3:     <add name="myPolicy">
   4:       <exceptionTypes>
   5:         <add name="InvalidUserNameException" type="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling"
   6:              postHandlingAction="ThrowNewException">
   7:          ...
   8:         <add name="UserNamePasswordNotMatchException" type="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling"
   9:              postHandlingAction="ThrowNewException">
  10:           ...
  11:         </add>          
  12:       </exceptionTypes>
  13:     </add>
  14:   </exceptionPolicies>
  15: </exceptionHandling>

按照我们上面的异常处理策略,在这种情况下我们将采用“错误页面”的方式来进行异常处理。也HandleErrorAttribute的处理方式类似,我们支持异常类型和Error View之间的匹配关系,而这是通过类似于如下的配置来定义的。值得一提的是,这里的异常类型是经过处理后重新抛出的异常。

   1: <artech.exceptionHandling>
   2:   <add exceptionType="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling"
   3:         errorView="InvalideUserNameError"/>
   4:   <add exceptionType="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling"
   5:         errorView="UserNamePasswordNotMatchError"/>
   6: </artech.exceptionHandling>

如上面的配置所示,我们为InvalidUserNameException和UserNamePasswordNotMatchException这两种异常类型定义了不同的Error View,分别是“InvalideUserNameError”和“UserNamePasswordNotMatchError”,详细定义如下所示:

   1: @{
   2:     Layout = null;
   3: }
   4: <!DOCTYPE html>
   5: <html>
   6: <head>
   7:     <title>Error</title>
   8: </head>
   9: <body>
  10:     <p style="color:Red; font-weight:bold">Sorry,the user name you specify does not exist!</p>
  11: </body>
  12: </html>
  13:  
  14: @{
  15:     Layout = null;
  16: }
  17: <!DOCTYPE html>
  18: <html>
  19: <head>
  20:     <title>Error</title>
  21: </head>
  22: <body>
  23:     <p style="color:Red; font-weight:bold">Sorry, The password does not match the given user name!</p>
  24: </body>
  25: </html>

现在我们按照上面的方式运行我们的程序,在分别输入错误的用户名和密码的情况下会自动显现相应的错误页面。

四、自定义ActionInvoker:ExceptionActionInvoker

对于上述的两种不同的异常处理方式最终是通过自定义的ActionInvoker来实现的,我们将其命名为ExceptionActionInvoker。如下面的代码片断所式,ExceptionActionInvoker直接继承自ControllerActionInvoker。属性ExceptionPolicy是一个基于指定的异常策略名称创建的ExceptionPolicyImpl 对象,用于针对EntLib进行的异常处理。而属性GetErrorView是一个用于获得作为错误页面的ViewResult对象的委托。整个异常处理的核心定义在InvokeAction方法中,该方法中指定的handleErrorActionName参数代表的是“异常处理操作名称”,整个方法就是按照上述的异常处理策略实现的。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Web;
   5: using System.Web.Mvc;
   6: using Artech.Mvc.ExceptionHandling.Configuration;
   7: using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
   8: using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
   9: namespace Artech.Mvc.ExceptionHandling
  10: {
  11:     public class ExceptionActionInvoker: ControllerActionInvoker
  12:     {
  13:         protected ExceptionHandlingSettings ExceptionHandlingSettings{get; private set;}
  14:         protected virtual Func<string, HandleErrorInfo, ViewResult> GetErrorView { get; private set; }
  15:         public ExceptionPolicyImpl ExceptionPolicy { get; private set; }
  16:         public ExceptionActionInvoker(string exceptionPolicy,Func<string, HandleErrorInfo, ViewResult> getErrorView)
  17:         {
  18:             this.ExceptionPolicy = EnterpriseLibraryContainer.Current.GetInstance<ExceptionPolicyImpl>(exceptionPolicy);
  19:             this.GetErrorView = getErrorView;
  20:             this.ExceptionHandlingSettings = ExceptionHandlingSettings.GetSection();
  21:         }
  22:  
  23:         public override bool InvokeAction(ControllerContext controllerContext, string handleErrorActionName)
  24:         {
  25:             ExceptionContext exceptionContext = controllerContext as ExceptionContext;
  26:             if (null == exceptionContext)
  27:             {
  28:                 throw new ArgumentException("The controllerContext must be ExceptionContext!", "controllerContext");
  29:             }
  30:             try
  31:             {
  32:                 exceptionContext.ExceptionHandled = true;
  33:                 if (this.ExceptionPolicy.HandleException(exceptionContext.Exception))
  34:                 {
  35:                     HandleRethrownException(exceptionContext);
  36:                 }
  37:                 else
  38:                 {
  39:                     if (ExceptionHandlingContext.Current.Errors.Count == 0)
  40:                     {
  41:                         ExceptionHandlingContext.Current.Errors.Add(exceptionContext.Exception.Message);
  42:                     }
  43:                     ControllerDescriptor controllerDescriptor = this.GetControllerDescriptor(exceptionContext);
  44:                     ActionDescriptor handleErrorAction = FindAction(exceptionContext, controllerDescriptor, handleErrorActionName);
  45:                     if (null != handleErrorAction)
  46:                     {
  47:                         IDictionary<string, object> parameters = GetParameterValues(controllerContext, handleErrorAction);
  48:                         exceptionContext.Result = this.InvokeActionMethod(exceptionContext, handleErrorAction, parameters);
  49:                     }
  50:                     else
  51:                     {
  52:                         HandleRethrownException(exceptionContext);
  53:                     }
  54:                 }
  55:                 return true;
  56:             }
  57:             catch (Exception ex)
  58:             {
  59:                 exceptionContext.Exception = ex;
  60:                 HandleRethrownException(exceptionContext);
  61:                 return true;
  62:             }
  63:         }
  64:         protected virtual void HandleRethrownException(ExceptionContext exceptionContext)
  65:         {
  66:             string errorViewName = this.GetErrorViewName(exceptionContext.Exception.GetType());
  67:             string controllerName = (string)exceptionContext.RouteData.GetRequiredString("controller");
  68:             string action = (string)exceptionContext.RouteData.GetRequiredString("action");
  69:             HandleErrorInfo handleErrorInfo = new HandleErrorInfo(exceptionContext.Exception, controllerName, action);
  70:             exceptionContext.Result = this.GetErrorView(errorViewName, handleErrorInfo);
  71:         }
  72:         protected string GetErrorViewName(Type exceptionType)
  73:         {
  74:             ExceptionErrorViewElement element = ExceptionHandlingSettings.ExceptionErrorViews
  75:                 .Cast<ExceptionErrorViewElement>().FirstOrDefault(el=>el.ExceptionType == exceptionType);
  76:             if(null != element)
  77:             {
  78:                 return element.ErrorView;
  79:             }
  80:             if(null== element && null != exceptionType.BaseType!= null)
  81:             {
  82:                 return GetErrorViewName(exceptionType.BaseType);
  83:             }
  84:             else
  85:             {
  86:                 return "Error";
  87:             }
  88:         }
  89:     }
  90: }

五、自定义Controller:BaseController

ExceptionActionInvoker最终在我们自定义的Controller基类BaseController中被调用的。ExceptionActionInvoker对象在构造函数中被初始化,并在重写的OnException方法中被调用。

   1: using System;
   2: using System.Web.Mvc;
   3: namespace Artech.Mvc.ExceptionHandling
   4: {
   5:     public abstract class BaseController : Controller
   6:     {
   7:         public BaseController(string exceptionPolicy)
   8:         {
   9:             Func<string, HandleErrorInfo, ViewResult> getErrorView = (viewName, handleErrorInfo) => this.View(viewName, handleErrorInfo);
  10:             this.ExceptionActionInvoker = new ExceptionActionInvoker(exceptionPolicy,getErrorView);
  11:         }
  12:         public BaseController(ExceptionActionInvoker actionInvoker)
  13:         {
  14:             this.ExceptionActionInvoker = actionInvoker;
  15:         }
  16:  
  17:         public virtual ExceptionActionInvoker ExceptionActionInvoker { get; private set; }
  18:  
  19:         protected virtual string GetHandleErrorActionName(string actionName)
  20:         {
  21:             return string.Format("On{0}Error", actionName);
  22:         }
  23:  
  24:         protected override void OnException(ExceptionContext filterContext)
  25:         {
  26:             using (ExceptionHandlingContextScope contextScope = new ExceptionHandlingContextScope(filterContext))
  27:             {
  28:                 string actionName = RouteData.GetRequiredString("action");
  29:                 string handleErrorActionName = this.GetHandleErrorActionName(actionName);
  30:                 this.ExceptionActionInvoker.InvokeAction(filterContext, handleErrorActionName);
  31:                 foreach (var error in ExceptionHandlingContext.Current.Errors)
  32:                 {
  33:                     ModelState.AddModelError(Guid.NewGuid().ToString() ,error.ErrorMessage);
  34:                 }
  35:             }
  36:         }
  37:     }
  38: }

值得一提的是:整个OnException方法中的操作都在一个ExceptionHandlingContextScope中进行的。顾名思义, 我们通过ExceptionHandlingContextScope为ExceptionHandlingContext创建了一个范围。ExceptionHandlingContext定义如下,我们可以通过它获得当前的ExceptionContext和ModelErrorCollection,而静态属性Current返回当前的ExceptionHandlingContext对象。

   1: public class ExceptionHandlingContext
   2: {
   3:     [ThreadStatic]
   4:     private static ExceptionHandlingContext current;
   5:  
   6:     public ExceptionContext ExceptionContext { get; private set; }
   7:     public ModelErrorCollection Errors { get; private set; }
   8:  
   9:     public ExceptionHandlingContext(ExceptionContext exceptionContext)
  10:     {
  11:         this.ExceptionContext = exceptionContext;
  12:         this.Errors = new ModelErrorCollection();
  13:     }
  14:     public static ExceptionHandlingContext Current
  15:     {
  16:         get { return current; }
  17:         set { current = value; }
  18:     }
  19: }

在BaseController的OnException方法中,当执行了ExceptionActionInvoker的InvokeAction之后,我们会将当前ExceptionHandlingContext的ModelError转移到当前的ModelState中。这就是为什么我们会通过ValidationSummary显示错误信息的原因。对于我们的例子来说,错误消息的指定是通过如下所示的ErrorMessageSettingHandler 实现的,而它仅仅将指定的错误消息添加到当前ExceptionHandlingContext的Errors属性集合中而已。

   1: [ConfigurationElementType(typeof(ErrorMessageSettingHandlerData))]
   2: public class ErrorMessageSettingHandler : IExceptionHandler
   3: {
   4:     public string ErrorMessage { get; private set; }
   5:     public ErrorMessageSettingHandler(string errorMessage)
   6:     {
   7:         this.ErrorMessage = errorMessage;
   8:     }
   9:     public Exception HandleException(Exception exception, Guid handlingInstanceId)
  10:     {
  11:         if (null == ExceptionHandlingContext.Current)
  12:         {
  13:             throw new InvalidOperationException("...");
  14:         }
  15:  
  16:         if (string.IsNullOrEmpty(this.ErrorMessage))
  17:         {
  18:             ExceptionHandlingContext.Current.Errors.Add(exception.Message);
  19:         }
  20:         else
  21:         {
  22:             ExceptionHandlingContext.Current.Errors.Add(this.ErrorMessage);
  23:         }
  24:         return exception;
  25:     }
  26: }  

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

一个简易版的T4代码生成"框架"

对于企业开发来说,代码生成在某种意义上可以极大地提高开发效率和质量。在众多代码生成方案来说,T4是一个不错的选择,今天花了点时间写了一个简易版本的T4代码生成的...

2127
来自专栏技术小讲堂

Unity Container中的几种注册方式与示例1.实例注册2.简单类型注册

1.实例注册 最简单的注册方式就是实例注册,Unity 容器负责维护对一个类型的单例引用,比如: 有如下的实际类型: namespace ConsoleSamp...

3306
来自专栏技术博客

Asp.Net Web API 2第十六课——Parameter Binding in ASP.NET Web API(参数绑定)

阅读本文之前,您也可以到Asp.Net Web API 2 系列导航进行查看 http://www.cnblogs.com/aehyok/p/3446289.h...

953
来自专栏c#开发者

BizTalk Orchestration execute Flat file disassembler ReceivePipeline

需求是这样,在一个inbound XML中有个一点节点使用平文件的方式存放,还要解析比如固定长度或根据特殊字符截取字段 也就是需要在流程里面先把输入的XML的节...

36913
来自专栏林德熙的博客

UWP IRandomAccessStream 与 Stream 互转

本文告诉大家如何将 IRandomAccessStream 和 Stream 互转

1522
来自专栏JAVA同学会

Spring Data(二)查询

查询的构建机制对于Spring Data的基础是非常有用的。构建的机制将截断前缀find…By、read…By、query…By、count…By、get…By...

932
来自专栏盛国存的专栏

A Bite of GoLang(中)

上述的 v 就称为局部变量, sum 称为自由变量,`func(v int) int {

6167
来自专栏跟着阿笨一起玩NET

事件(Event)

事件是对象发送的消息,以发信号通知操作的发生。操作可能是由用户交互(例如鼠标单击)引起的,也可能是由某些其他的程序逻辑触发的。引发事件的对象称为事件发送方。捕获...

882
来自专栏Python

仿照wtform自定义Form组件

仿照wtforms自定义Form组件 1.wtforms 点击查看源码分析及使用方法 2.自定义Form组件 #!usr/bin/env python # -*...

2257
来自专栏蘑菇先生的技术笔记

多线程中的锁系统(一)-基础用法

2485

扫码关注云+社区

领取腾讯云代金券