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。
namespaceArtech.Mvc.ExceptionHandling.Models { publicclassLoginInfo { [Display(Name = User Name )] [Required(ErrorMessage = User Name is manadatory! )] publicstringUserName { get; set; } [Display(Name = Password )] [DataType(DataType.Password)] [Required(ErrorMessage = Password is manadatory! )] publicstringPassword { get; set; } } }
们定义了如下一个AccountController,它是们自定义的BaseController的子类。AccountController在构造的时候调用基类构造函数指定的参数代表异常处理策略的配置名称。SignIn方法代表用于进行 登录 的操作,而OnSignInError就表示该操作对应的异常处理操作。如果在SignIn操作中抛出的异常经过处理后无需再抛出,则会通过调用OnSignInError,而此时ModelState已经被设置了相应的错误消息。
publicclassAccountController : BaseController { publicAccountController() base( myPolicy )5:{ } publicActionResult SignIn() { returnView(newLoginInfo()); } [HttpPost] publicActionResult SignIn(LoginInfo loginInfo) { if(!ModelState.IsValid) { returnthis.View(newLoginInfo { UserName = loginInfo.UserName }); } if(loginInfo.UserName != Foo ) { thrownewInvalidUserNameException(); } if(loginInfo.Password != password ) { thrownewUserNamePasswordNotMatchException(); } ViewBag.Message = Authentication Succeeds! ; returnthis.View(newLoginInfo { UserName = loginInfo.UserName }); } publicActionResult OnSignInError(stringuserName) { returnthis.View(newLoginInfo { UserName = userName }); } }
具体定义在SignIn操作方法中的认证逻辑是这样的:如果用户名不是 Foo 则抛出InvalidUserNameException异常;如果密码不是 password 则抛出UserNamePasswordNotMatchException异常。下面是SignIn操作对应的View的定义:
@model Artech.Mvc.ExceptionHandling.Models.LoginInfo @{ ViewBag.Title = SignIn ; } @Html.ValidationSummary() @if (ViewBag.Messages != null) { @ViewBag.Messages } @using (Html.BeginForm()) { @Html.EditorForModel() inputtype= submit value= SignIn / }
在AccountController初始化时指定的异常处理策略 myPolicy 定义在如下的配置中。们专门针对SignIn操作方法抛出的InvalidUserNameException和UserNamePasswordNotMatchException进行了处理,而ErrorMessageSettingHandler是们自定义的异常处理器,它仅仅用于设置错误消息。如下面的代码片断所示,如果上述的这两种类型的异常被抛出,最终的错误消息会被指定为 User name does not exist! 和 User name does not match password! 。
exceptionHandling exceptionPolicies addnameaddname= myPolicy exceptionTypes addnameaddname= InvalidUserNameException type= Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling postHandlingAction= None exceptionHandlers addnameaddname= ErrorMessageSettingHandler type= Artech.Mvc.ExceptionHandling.ErrorMessageSettingHandler, Artech.Mvc.ExceptionHandling errorMessage= User name does not exist! / /exceptionHandlers /add addnameaddname= UserNamePasswordNotMatchException type= Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling postHandlingAction= None exceptionHandlers addnameaddname= ErrorMessageSettingHandler type= Artech.Mvc.ExceptionHandling.ErrorMessageSettingHandler, Artech.Mvc.ExceptionHandling errorMessage= User name does not match password! / /exceptionHandlers /add /exceptionTypes /add /exceptionPolicies /exceptionHandling
现在们通过路由映射将AccountController和Sign设置为默认Controller和Action后,开启们的应用程序。在输入错误的用户名和错误明码的情况下在ValidationSummary中将自动得到相应的错误消息。
三、通过配置的Error View处理异常
在上面的配置中,针对InvalidUserNameException和UserNamePasswordNotMatchException这两种异常类型的配置策略都将PostHandlingAction属性设置为 None ,意味着不会将原来的异常和处理后的异常进行重新抛出。现在们将该属性设置为 ThrowNewException ,意味着们会将处理后的异常重新抛出来。
exceptionHandling exceptionPolicies addnameaddname= myPolicy exceptionTypes addnameaddname= InvalidUserNameException type= Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling postHandlingAction= ThrowNewException ... addnameaddname= UserNamePasswordNotMatchException type= Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling 9:postHandlingAction= ThrowNewException ... /add /exceptionTypes /add /exceptionPolicies /exceptionHandling
按照们上面的异常处理策略,在这种情况下们将采用 错误页面 的方式来进行异常处理。也HandleErrorAttribute的处理方式类似,们支持异常类型和Error View之间的匹配关系,而这是通过类似于如下的配置来定义的。值得一提的是,这里的异常类型是经过处理后重新抛出的异常
exceptionHandling exceptionPolicies addnameaddname= myPolicy exceptionTypes addnameaddname= InvalidUserNameException type= Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling postHandlingAction= ThrowNewException ... addnameaddname= UserNamePasswordNotMatchException type= Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling 9:postHandlingAction= ThrowNewException ... /add /exceptionTypes /add /exceptionPolicies /exceptionHandling
如上面的配置所示,们为InvalidUserNameException和UserNamePasswordNotMatchException这两种异常类型定义了不同的Error View,分别是 InvalideUserNameError 和 UserNamePasswordNotMatchError ,详细定义如下所示:
@{ Layout = null; } !DOCTYPEhtml html head title Error /title /head body pstylep >现在们按照上面的方式运行们的程序,在分别输入错误的用户名和密码的情况下会自动显现相应的错误页面。
四、自定义ActionInvoker:ExceptionActionInvoker
对于上述的两种不同的异常处理方式最终是通过自定义的ActionInvoker来实现的,们将其命名为ExceptionActionInvoker。如下面的代码片断所式,ExceptionActionInvoker直接继承自ControllerActionInvoker。属性ExceptionPolicy是一个基于指定的异常策略名称创建的ExceptionPolicyImpl 对象,用于针对EntLib进行的异常处理。而属性GetErrorView是一个用于获得作为错误页面的ViewResult对象的委托。整个异常处理的核心定义在InvokeAction方法中,该方法中指定的handleErrorActionName参数代表的是 异常处理操作名称 ,整个方法就是按照上述的异常处理策略实现的。
usingSystem; usingSystem.Collections.Generic; usingSystem.Linq; usingSystem.Web; usingSystem.Web.Mvc; usingArtech.Mvc.ExceptionHandling.Configuration; usingMicrosoft.Practices.EnterpriseLibrary.Common.Configuration; usingMicrosoft.Practices.EnterpriseLibrary.ExceptionHandling; namespaceArtech.Mvc.ExceptionHandling { publicclassExceptionActionInvoker: ControllerActionInvoker { protectedExceptionHandlingSettings ExceptionHandlingSettings{get; privateset;} protectedvirtualFunc string, HandleErrorInfo, ViewResult GetErrorView { get; privateset; } publicExceptionPolicyImpl ExceptionPolicy { get; privateset; } publicExceptionActionInvoker(stringexceptionPolicy,Func string, HandleErrorInfo, ViewResult getErrorView) { this.ExceptionPolicy = EnterpriseLibraryContainer.Current.GetInstance ExceptionPolicyImpl (exceptionPolicy); this.GetErrorView = getErrorView; this.ExceptionHandlingSettings = ExceptionHandlingSettings.GetSection(); } publicoverrideboolInvokeAction(ControllerContext controllerContext, stringhandleErrorActionName) { ExceptionContext exceptionContext = controllerContext asExceptionContext; if(null== exceptionContext) { thrownewArgumentException( The controllerContext must be ExceptionContext! , controllerContext ); } try { exceptionContext.ExceptionHandled = true; if(this.ExceptionPolicy.HandleException(exceptionContext.Exception)) { HandleRethrownException(exceptionContext); } else { if(ExceptionHandlingContext.Current.Errors.Count == 0) { ExceptionHandlingContext.Current.Errors.Add(exceptionContext.Exception.Message); } ControllerDescriptor controllerDescriptor = this.GetControllerDescriptor(exceptionContext); ActionDescriptor handleErrorAction = FindAction(exceptionContext, controllerDescriptor, handleErrorActionName); if(null!= handleErrorAction) { IDictionary string, object parameters = GetParameterValues(controllerContext, handleErrorAction); exceptionContext.Result = this.InvokeActionMethod(exceptionContext, handleErrorAction, parameters); } else { HandleRethrownException(exceptionContext); } } returntrue; } catch(Exception ex) { exceptionContext.Exception = ex;60:HandleRethrownException(exceptionContext); returntrue; } } protectedvirtualvoidHandleRethrownException(ExceptionContext exceptionContext) { stringerrorViewName = this.GetErrorViewName(exceptionContext.Exception.GetType()); stringcontrollerName = (string)exceptionContext.RouteData.GetRequiredString( controller ); stringaction = (string)exceptionContext.RouteData.GetRequiredString( action ); HandleErrorInfo handleErrorInfo = newHandleErrorInfo(exceptionContext.Exception, controllerName, action);70:exceptionContext.Result = this.GetErrorView(errorViewName, handleErrorInfo); } protectedstringGetErrorViewName(Type exceptionType) { ExceptionErrorViewElement element = ExceptionHandlingSettings.ExceptionErrorViews75:.Cast ExceptionErrorViewElement ().FirstOrDefault(el= el.ExceptionType == exceptionType); if(null!= element) { returnelement.ErrorView; } if(null== element null!= exceptionType.BaseType!= null) { returnGetErrorViewName(exceptionType.BaseType); } else { return Error ; } } } }
五、自定义Controller:BaseController
ExceptionActionInvoker最终在们自定义的Controller基类BaseController中被调用的。ExceptionActionInvoker对象在构造函数中被初始化,并在重写的OnException方法中被调用。
usingSystem; usingSystem.Web.Mvc; namespaceArtech.Mvc.ExceptionHandling { publicabstractclassBaseController : Controller { publicBaseController(stringexceptionPolicy) { Func string, HandleErrorInfo, ViewResult getErrorView = (viewName, handleErrorInfo) = this.View(viewName, handleErrorInfo); this.ExceptionActionInvoker = newExceptionActionInvoker(exceptionPolicy,getErrorView); } publicBaseController(ExceptionActionInvoker actionInvoker) { this.ExceptionActionInvoker = actionInvoker;}publicvirtualExceptionActionInvoker ExceptionActionInvoker { get; privateset; } protectedvirtualstringGetHandleErrorActionName(stringactionName) { returnstring.Format( On{0}Error , actionName); } protectedoverridevoidOnException(ExceptionContext filterContext) { using(ExceptionHandlingContextScope contextScope = newExceptionHandlingContextScope(filterContext)) { stringactionName = RouteData.GetRequiredString( action ); stringhandleErrorActionName = this.GetHandleErrorActionName(actionName); this.ExceptionActionInvoker.InvokeAction(filterContext, handleErrorActionName); foreach(var error inExceptionHandlingContext.Current.Errors) { ModelState.AddModelError(Guid.NewGuid().ToString() ,error.ErrorMessage); } } } } }
值得一提的是:整个OnException方法中的操作都在一个ExceptionHandlingContextScope中进行的。顾名思义, 们通过ExceptionHandlingContextScope为ExceptionHandlingContext创建了一个范围。ExceptionHandlingContext定义如下,们可以通过它获得当前的ExceptionContext和ModelErrorCollection,而静态属性Current返回当前的ExceptionHandlingContext对象。
publicclassExceptionHandlingContext { [ThreadStatic] privatestaticExceptionHandlingContext current; publicExceptionContext ExceptionContext { get; privateset; } publicModelErrorCollection Errors { get; privateset; } publicExceptionHandlingContext(ExceptionContext exceptionContext) { this.ExceptionContext = exceptionContext;this.Errors = newModelErrorCollection(); } publicstaticExceptionHandlingContext Current { get { returncurrent; } set { current = value;} } }
在BaseController的OnException方法中,当执行了ExceptionActionInvoker的InvokeAction之后,们会将当前ExceptionHandlingContext的ModelError转移到当前的ModelState中。这就是为什么们会通过ValidationSummary显示错误信息的原因。对于们的例子来说,错误消息的指定是通过如下所示的ErrorMessageSettingHandler 实现的,而它仅仅将指定的错误消息添加到当前ExceptionHandlingContext的Errors属性集合中而已。
[ConfigurationElementType(typeof(ErrorMessageSettingHandlerData))] publicclassErrorMessageSettingHandler : IExceptionHandler { publicstringErrorMessage { get; privateset; } publicErrorMessageSettingHandler(stringerrorMessage) { this.ErrorMessage = errorMessage; } publicException HandleException(Exception exception, Guid handlingInstanceId) { if(null== ExceptionHandlingContext.Current) { thrownewInvalidOperationException( ... ); } if(string.IsNullOrEmpty(this.ErrorMessage)) { ExceptionHandlingContext.Current.Errors.Add(exception.Message); } else { ExceptionHandlingContext.Current.Errors.Add(this.ErrorMessage); } returnexception; } }