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方法默認采用“On{Action}Error”這樣的命名規則,而當前上下文會與異常處理操作方法的參數進行綁定。除次之外,我們會設置當前ModelState的錯誤信息;
- 如果用戶不曾定義相應的異常處理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方法代表用於進行“登錄”的操作,而就表示該操作對應的異常處理操作。如果在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中將自動得到相應的錯誤消息。
在上麵的配置中,針對InvalidUserNameException和UserNamePasswordNotMatchException這兩種異常類型的配置策略都將PostHandlingAction屬性設置為“None”,意味著不會將原來的異常和處理後的異常進行重新拋出。現在我們將該屬性設置為“”,意味著我們會將處理後的異常重新拋出來。
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。如下麵的代碼片斷所式,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: }
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: }
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 14:04:46