ASP.NET MVC是一個極具可擴充開發架構,在這篇文章中我將通過它的擴充實現與EntLib的整合,並提供一個完整的解決異常處理解決方案。
EntLib的異常處理應用塊(Exception Handling Application Block)是一個不錯的異常處理架構,它使我們可以採用配置的方式來定義異常處理策略。而ASP.NET MVC是一個極具可擴充開發架構,在這篇文章中我將通過它的擴充實現與EntLib的整合,並提供一個完整的解決異常處理解決方案。
一、基本異常處理策略
我們首先來討論我們的解決方案具體採用的異常處理策略:
對於執行Controller的某個Action方法拋出的異常,我們會按照指定配置策略進行處理。我們可以採取日誌記錄、異常替換和封裝這些常用的異常處理方式;
對於處理後的異常,如果異常處理策略規定需要將其拋出,則會自動重新導向到與異常類型匹配的出錯頁面。我們會維護一個異常類型和Error View的匹配關係;
對於處理後的異常,如果異常處理策略規定不需要將其拋出,則會執行與當前Action操作相匹配的錯誤處理Action進行處理。異常處理Action方法預設採用“On{Action}Error”這樣的命名規則,而當前上下文會與異常處理操作方法的參數進行綁定。除次之外,我們會設定當前ModelState的錯誤資訊;
如果使用者不曾定義相應的異常處理Action,依然採用“錯誤頁面重新導向”方式進行異常處理。
二、通過自訂Action處理異常
為了讓讀者對上面介紹的異常處理頁面有一個深刻的理解,我們來進行一個執行個體示範。該執行個體用於類比使用者登入,我們定義了如下一個只包含使用者名稱和密碼兩個屬性的Model:LoginInfoModel。
namespace Artech.Mvc.ExceptionHandling.Models { public class LoginInfo { [Display(Name ="User Name")] [Required(ErrorMessage = "User Name is manadatory!")] public string UserName { get; set; } [Display(Name = "Password")] [DataType(DataType.Password)] [Required(ErrorMessage = "Password is manadatory!")] public string Password { get; set; } } }
我們定義了如下一個AccountController,它是我們自訂的BaseController的子類。AccountController在構造的時候調用基類建構函式指定的參數代表異常處理策略的配置名稱。SignIn方法代表用於進行“登入”的操作,而OnSignInError就表示該操作對應的異常處理操作。如果在SignIn操作中拋出的異常經過處理後無需再拋出,則會通過調用OnSignInError,而此時ModelState已經被設定了相應的錯誤訊息。
public class AccountController BaseController { public AccountController() base("myPolicy") { } public ActionResult SignIn() { return View(new LoginInfo()); } [HttpPost] public ActionResult SignIn(LoginInfo loginInfo) { if (!ModelState.IsValid) { return this.View(new LoginInfo { UserName = loginInfo.UserName }); } if (loginInfo.UserName != "Foo") { throw new InvalidUserNameException(); } if (loginInfo.Password != "password") { throw new UserNamePasswordNotMatchException(); } ViewBag.Message = "Authentication Succeeds!"; return this.View(new LoginInfo { UserName = loginInfo.UserName }); } public ActionResult OnSignInError(string userName) { return this.View(new LoginInfo { 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() <input type="submit" value="SignIn" /> }
在AccountController初始化時指定的異常處理策略“myPolicy”定義在如下的配置中。我們專門針對SignIn操作方法拋出的InvalidUserNameException和UserNamePasswordNotMatchException進行了處理,而ErrorMessageSettingHandler是我們自訂的異常處理器,它僅僅用於設定錯誤訊息。如下面的代碼片斷所示,如果上述的這兩種類型的異常被拋出,最終的錯誤訊息會被指定為“User name does not exist!”和“User name does not match password!”。
<exceptionHandling> <exceptionPolicies> <add name="myPolicy"> <exceptionTypes> <add name="InvalidUserNameException" type="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling" postHandlingAction="None"> <exceptionHandlers> <add name="ErrorMessageSettingHandler" type="Artech.Mvc.ExceptionHandling.ErrorMessageSettingHandler, Artech.Mvc.ExceptionHandling" errorMessage="User name does not exist!"/> </exceptionHandlers> </add> <add name="UserNamePasswordNotMatchException" type="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling" postHandlingAction="None"> <exceptionHandlers> <add name="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> <add name="myPolicy"> <exceptionTypes> <add name="InvalidUserNameException" type="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling" postHandlingAction="ThrowNewException"> ... <add name="UserNamePasswordNotMatchException" type="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling" postHandlingAction="ThrowNewException"> ... </add> </exceptionTypes> </add> </exceptionPolicies> </exceptionHandling>
按照我們上面的異常處理策略,在這種情況下我們將採用“錯誤頁面”的方式來進行異常處理。也HandleErrorAttribute的處理方式類似,我們支援異常類型和Error View之間的匹配關係,而這是通過類似於如下的配置來定義的。值得一提的是,這裡的異常類型是經過處理後重新拋出的異常。
<artech.exceptionHandling> <add exceptionType="Artech.Mvc.ExceptionHandling.Models.InvalidUserNameException, Artech.Mvc.ExceptionHandling" errorView="InvalideUserNameError"/> <add exceptionType="Artech.Mvc.ExceptionHandling.Models.UserNamePasswordNotMatchException, Artech.Mvc.ExceptionHandling" errorView="UserNamePasswordNotMatchError"/> </artech.exceptionHandling>
如上面的配置所示,我們為InvalidUserNameException和UserNamePasswordNotMatchException這兩種異常類型定義了不同的Error View,分別是“InvalideUserNameError”和“UserNamePasswordNotMatchError”,詳細定義如下所示:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Error</title> </head> <body> <p style="colorRed; font-weightbold">Sorry,the user name you specify does not exist!</p> </body> </html> @{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Error</title> </head> <body> <p style="colorRed; font-weightbold">Sorry, The password does not match the given user name!</p> </body> </html>
現在我們按照上面的方式運行我們的程式,在分別輸入錯誤的使用者名稱和密碼的情況下會自動顯現相應的錯誤頁面。
四、自訂ActionInvoker:ExceptionActionInvoker
對於上述的兩種不同的異常處理方式最終是通過自訂的ActionInvoker來實現的,我們將其命名為ExceptionActionInvoker。如下面的代碼片斷所式,ExceptionActionInvoker直接繼承自ControllerActionInvoker。屬性ExceptionPolicy是一個基於指定的異常策略名稱建立的ExceptionPolicyImpl 對象,用於針對EntLib進行的異常處理。而屬性GetErrorView是一個用於獲得作為錯誤頁面的ViewResult對象的委託。整個異常處理的核心定義在InvokeAction方法中,該方法中指定的handleErrorActionName參數代表的是“異常處理操作名稱”,整個方法就是按照上述的異常處理策略實現的。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using Artech.Mvc.ExceptionHandling.Configuration; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling; namespace Artech.Mvc.ExceptionHandling { public class ExceptionActionInvoker ControllerActionInvoker { protected ExceptionHandlingSettings ExceptionHandlingSettings{get; private set;} protected virtual Func<string, HandleErrorInfo, ViewResult> GetErrorView { get; private set; } public ExceptionPolicyImpl ExceptionPolicy { get; private set; } public ExceptionActionInvoker(string exceptionPolicy,Func<string, HandleErrorInfo, ViewResult> getErrorView) { this.ExceptionPolicy = EnterpriseLibraryContainer.Current.GetInstance<ExceptionPolicyImpl>(exceptionPolicy); this.GetErrorView = getErrorView; this.ExceptionHandlingSettings = ExceptionHandlingSettings.GetSection(); } public override bool InvokeAction(ControllerContext controllerContext, string handleErrorActionName) { ExceptionContext exceptionContext = controllerContext as ExceptionContext; if (null == exceptionContext) { throw new ArgumentException("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); } } return true; } catch (Exception ex) { exceptionContext.Exception = ex; HandleRethrownException(exceptionContext); return true; } } protected virtual void HandleRethrownException(ExceptionContext exceptionContext) { string errorViewName = this.GetErrorViewName(exceptionContext.Exception.GetType()); string controllerName = (string)exceptionContext.RouteData.GetRequiredString("controller"); string action = (string)exceptionContext.RouteData.GetRequiredString("action"); HandleErrorInfo handleErrorInfo = new HandleErrorInfo(exceptionContext.Exception, controllerName, action); exceptionContext.Result = this.GetErrorView(errorViewName, handleErrorInfo); } protected string GetErrorViewName(Type exceptionType) { ExceptionErrorViewElement element = ExceptionHandlingSettings.ExceptionErrorViews .Cast<ExceptionErrorViewElement>().FirstOrDefault(el=>el.ExceptionType == exceptionType); if(null != element) { return element.ErrorView; } if(null== element && null != exceptionType.BaseType!= null) { return GetErrorViewName(exceptionType.BaseType); } else { return "Error"; } } } }
五、自訂Controller:BaseController
ExceptionActionInvoker最終在我們自訂的Controller基類BaseController中被調用的。ExceptionActionInvoker對象在建構函式中被初始化,並在重寫的OnException方法中被調用。
using System; using System.Web.Mvc; namespace Artech.Mvc.ExceptionHandling { public abstract class BaseController Controller { public BaseController(string exceptionPolicy) { Func<string, HandleErrorInfo, ViewResult> getErrorView = (viewName, handleErrorInfo) => this.View(viewName, handleErrorInfo); this.ExceptionActionInvoker = new ExceptionActionInvoker(exceptionPolicy,getErrorView); } public BaseController(ExceptionActionInvoker actionInvoker) { this.ExceptionActionInvoker = actionInvoker; } public virtual ExceptionActionInvoker ExceptionActionInvoker { get; private set; } protected virtual string GetHandleErrorActionName(string actionName) { return string.Format("On{0}Error", actionName); } protected override void OnException(ExceptionContext filterContext) { using (ExceptionHandlingContextScope contextScope = new ExceptionHandlingContextScope(filterContext)) { string actionName = RouteData.GetRequiredString("action"); string handleErrorActionName = this.GetHandleErrorActionName(actionName); this.ExceptionActionInvoker.InvokeAction(filterContext, handleErrorActionName); foreach (var error in ExceptionHandlingContext.Current.Errors) { ModelState.AddModelError(Guid.NewGuid().ToString() ,error.ErrorMessage); } } } } }
值得一提的是:整個OnException方法中的操作都在一個ExceptionHandlingContextScope中進行的。顧名思義, 我們通過ExceptionHandlingContextScope為ExceptionHandlingContext建立了一個範圍。ExceptionHandlingContext定義如下,我們可以通過它獲得當前的ExceptionContext和ModelErrorCollection,而靜態屬性Current返回當前的ExceptionHandlingContext對象。
public class ExceptionHandlingContext { [ThreadStatic] private static ExceptionHandlingContext current; public ExceptionContext ExceptionContext { get; private set; } public ModelErrorCollection Errors { get; private set; } public ExceptionHandlingContext(ExceptionContext exceptionContext) { this.ExceptionContext = exceptionContext; this.Errors = new ModelErrorCollection(); } public static ExceptionHandlingContext Current { get { return current; } set { current = value; } } }
在BaseController的OnException方法中,當執行了ExceptionActionInvoker的InvokeAction之後,我們會將當前ExceptionHandlingContext的ModelError轉移到當前的ModelState中。這就是為什麼我們會通過ValidationSummary顯示錯誤資訊的原因。對於我們的例子來說,錯誤訊息的指定是通過如下所示的ErrorMessageSettingHandler 實現的,而它僅僅將指定的錯誤訊息添加到當前ExceptionHandlingContext的Errors屬性集合中而已。
[ConfigurationElementType(typeof(ErrorMessageSettingHandlerData))] public class ErrorMessageSettingHandler IExceptionHandler { public string ErrorMessage { get; private set; } public ErrorMessageSettingHandler(string errorMessage) { thisErrorMessage = errorMessage; } public Exception HandleException(Exception exception, Guid handlingInstanceId) { if (null == ExceptionHandlingContextCurrent) { throw new InvalidOperationException(""); } if (stringIsNullOrEmpty(thisErrorMessage)) { ExceptionHandlingContextCurrentErrorsAdd(exceptionMessage); } else { ExceptionHandlingContextCurrentErrorsAdd(thisErrorMessage); } return exception; } }
【相關推薦】
1.ASP免費視頻教程
2.ASP教程
3.李炎恢ASP基礎視頻教程