介紹
“MVC網站教程”系列的目的是教你如何使用 ASP.NET MVC 創建一個基本的、可擴展的網站。
2) MVC網站教程(二):異常管理
3) MVC網站教程(三):動態布局和站點管理(涉及技術:AJAX、jqGrid、Controller擴展、HTML Helpers等等)
4) MVC網站教程(四):MVC4網站中集成jqGrid表格插件(涉及技術:AJAX,JSON,jQuery,LINQ和序列化)
系列的第一篇文章“多語言網站框架”,主要講解如何去創建一個多語言MVC網站,同時也講解了用戶認證和注冊機制的實現。使用了微軟的Entity Framework框架和LINQ查詢技術。
系列的第二篇文章(即本文),制定異常管理規則並在ASP.NET MVC網站中實現異常管理,還提供一些通用的日志記錄和異常管理的源代碼。這些源代碼不僅可以在任何ASP.NET網站中被重用(或經過比較小的改動適用),而且可以重用到任何.NET項目中。
“MVC網站教程”系列的示例網站是采用增量式和迭代式軟件過程開發的,這意味着系列中每一篇博文會在前一篇的解決方案中添加更多的功能,所以本文提供的示例下載只包含系列第一篇和第二篇中所述的功能點。
在開發軟件解決方案中異常管理是非常重要的。如果忽視或者以錯誤方式實現異常管理,將嚴重影響軟件解決方案的質量和性能。
軟件環境
1. .NET 4.0 Framework
2. Visual Studio 2010(or Express edition)
3. ASP.NET MVC 4.0
4. SQL Server 2008 R2(or Express Edition version 10.50.2500.0, or higher version)
在運行示例代碼之前
在運行示例代碼之前,你應該做下面事情:
1. 首先使用“管理員身份”運行CreateEventLogEntry控制台項目程序產生的exe,用來在事件日志中創建“MVC Basic”事件源。(EventLog在寫日志時會創建類別默認為“應用程序”指定名稱的事件源。但是ASP.NET網站沒有足夠的權限來創建事件源)
2. 在你的SQL Server服務器中創建一個名為MvcBasicSite的數據庫,然后用我提供的MvcBasicSiteDatabase.bak文件進行數據庫還原。
3. 修改MVC應用程序示例的Web.config配置文件中的鏈接字符串。
本博文示例下載:
1) ASP.NET MVC 4.0 For VS2010 安裝文件 (安裝比較耗時,我安裝了2個小時)
MVC網站中的異常管理規則
在每個軟件項目中,在項目開發的開始階段,團隊應該定義軟件開發中必須遵守的規則。這些規則中應該包含異常管理規則。
在本篇中,我將描述示例MVC解決方案中使用的異常管理規則,並且這些規則可以作為實際項目的參考。能在任何ASP.NET網站中被重用(或經過比較小的改動適用),而且可以用重用到任何.NET項目中。
一般的異常管理機制都是使用try…catch…finally…或者using語句來管理執行可能會失敗的操作(如:訪問數據庫表、從文件系統訪問文件、使用內存分配、發送電子郵件等等),以合理的方式處理失敗並且釋放被占用的資源。注意,.NET Framework框架、第三方類庫、在程序代碼中使用throw關鍵字都能產生異常。
在示例MVC網站中使用的異常管理規則有:
1. 使用finally塊來釋放各種不可釋放的資源,這些資源是沒有實現IDisposable接口的但是又需要一些釋放操作,比如:close()掉在try塊中打開的Stream或文件。
2. 訪問可釋放資源時可能會產生異常,不要忘記使用try…catch…finally…或using語句來釋放占用的資源。
3. 不要在不需要的地方使用try…catch…吞掉異常,可以考慮使用if…else…語句避免異常發生從而提高程序性能。比如,在可能為空的對象上執行任何操作之前做非空判斷,能顯著的提高應用程序性能。
4. 不要重復catch和重復throw相同類型的異常。
5. 為整個解決方案定義一個派生自ApplicationException類的新異常類。在示例程序中,我們定義了BasicSiteException來標識是數據庫或邏輯層產生的異常。
6. 為整個解決方案提供一個公用的日志類,該類將異常信息寫到Windows系統的“事件日志”服務中。在本示例中這個類命名為MvcBasicLog。
7. 捕獲數據庫操作或邏輯層中產生的異常,僅在需要添加更多信息的時候使用MvcBasicException類再次拋出異常。注意不要忘記將原始異常做為再次拋出異常的InnerException屬性值。
8. 預期的異常應該在用戶界面層被捕獲並處理,然后異常信息(消息和堆棧跟蹤)必須使用MvcBasicLog類寫入Windows的事件日志中,並且提供一個友好的錯誤信息顯示在當前頁面給用戶查看。
9. 管理未經授權訪問的異常,來防止用戶在未經過身份驗證或沒有對應的訪問權限時,訪問站點的主要功能。
10. 注意,不要忘記在用戶界面層處理非預期異常,同樣,錯誤信息也必須寫進Windows的“事件日志”中,同時在錯誤頁面(Error.cshtml)上顯示一條友好的錯誤信息。
11. 通過使用適當的方法來避免產生不必要的未處理異常。比如,在本示例中使用FirstOrDefault()而不是First(),然后判斷是否為null,就像下面代碼:(First()在對空序列操作時,會拋出異常。而FirstOrDefault()在操作空序列時會返回default(TSource))----可參見:Linq入門詳解(Linq to Objects)
if (ModelState.IsValid) { // // Verify the user name and password. // User user = _db.Users.FirstOrDefault(item => item.Username.ToLower() == model.Username.ToLower() && item.Password == model.Password); if (user == null) { ModelState.AddModelError("", Resources.Resource.LogOnErrorMessage); // return View(model); } else { // // User logined succesfully ==> create a new site session! // FormsAuthentication.SetAuthCookie(model.Username, false); // SiteSession siteSession = new SiteSession(_db, user); Session["SiteSession"] = siteSession; // Cache the user login data! // // Log a message about the user LogOn. // MvcBasicLog.LogMessage(string.Format("LogOn for user: {0}", siteSession.Username)); // // Redirect to Home page. // return RedirectToAction("Index", "Home"); } }
將消息和異常寫入日志
在任何軟件解決方案中,將軟件使用過程中產生的信息、錯誤、異常數據存入日志中是非常重要的。這樣,這些信息和異常數據就能用於日后訪問和數據分析。在Windows系統中保存日志消息最適合的地方是 Windows Event Log 服務。
MvcBasicLog類是一個公用類,用於將消息和異常信息寫入Windows Event Log服務。
如上面類圖中所見,這個類包含了一系列公共的靜態方法用於在Windows event log 服務中記錄消息、錯誤、異常信息。所有的消息將被寫入到同一個日志源(注冊的日志源名存在_logSource靜態成員中),並且有些方法提供一個類別字符串參數,讓用戶能指定消息的前綴。
所有這些公共方法都使用AddLogLine私有方法來將消息寫到事件日志源中。
private static void AddLogLine(string logMessage, bool isError) { EventLog log = new EventLog(); log.Source = _logSource; // try { log.WriteEntry(logMessage, (isError ? EventLogEntryType.Error : EventLogEntryType.Information)); } catch (System.Security.SecurityException ex) { // // In Web app you do not have right to create event log source and // the log source must be created first by using the provided CreateEventLogEntry project! // throw new ApplicationException("You must create the event log entry " + "for our source by using CreateEventLogEntry project!", ex); } catch { // // The log file is to large, so clear it first. // log.Clear(); log.WriteEntry(logMessage, (isError ? EventLogEntryType.Error : EventLogEntryType.Information)); } // log.Close(); }
我們可以使用下面語句將消息寫到日志源中。
MvcBasicLog.LogMessage(string.Format("LogOn for user: {0}", siteSession.Username));
可以通過“Event Viewer”工具查看Windows的事件日志。(右鍵“我的電腦”-“管理”-“事件查看器”-“Windows日志”)
注意示例中的LogException方法,會將整個異常數據都寫入事件日志中,結果如下圖:
從上圖,你能看到事件日志的詳細異常消息、異常堆棧跟蹤以及產生異常的源代碼行。
所以,在程序使用期間產生的每個“錯誤”都會記錄在事件日志中,開發人員可以非常容易的確認錯誤產生的代碼行以及問題的上下文環境。
預期異常管理
在源代碼中,當你實現的一些操作可能存在失敗的情況下,比如訪問數據庫表、從文件系統中訪問文件、分配內存、發送電子郵件等等,這些操作會產生預期的異常。同樣,在實現的邏輯代碼中如果違反應用程序的邏輯規則時(比如,對未授權的頁面進行訪問),也會拋出預期異常。
預期異常是異常管理過程中重要的部分。為了實現該功能,我們創建一個適當的繼承自特定的異常基類的異常類。
異常管理過程中其他比較重要的方面是:許多預期異常,產生自原代碼的底層,關於這個問題的消息通知應該被顯示到用戶界面層,所以原始異常的消息必須一層一層的傳遞直到在用戶界面層被處理。在傳遞異常數據的過程中,會積累越來越多的上下文信息,這些信息將用於描述和定位問題,並且全部保存在日志中,以便將來使用(如:日志報表和解決問題)。
我使用MvcBasicException基類來管理預期異常。這個類繼承自ApplicationException基類,並且標注[Serializable]特性以支持序列化。
注意:序列化是經常發生的,但是卻常常不為人知。序列化機制是所有跨應用程序域調用的基礎,甚至還會發生在同一個進程內,比如從邏輯層傳遞數據到表現層。因此,為了保存和在各層之間傳遞整個異常數據(異常信息和堆棧跟蹤),我們必須為我們的異常類標注[Serializable]特性,同時還必須為支持反序列化提供一個受保護的構造函數。
如類圖中所見,這個類有三個構造函數,被用於以下場景:
1) 受保護的構造函數使用序列化數據創建一個新的MvcBasicException類實例。這個構造函數被用於在應用程序各層之間傳遞所有異常數據。
2) 使用特定的錯誤信息創建一個新的MvcBasicException類實例,這個特定信息應該能從應用程序角度說明異常的原因。
3) 使用特定的錯誤信息和原始異常引用來創建一個新的MvcBasicException類實例。第一個參數是一個錯誤信息,它能從應用程序角度說明異常的原因;第二個參數是當前異常的引用。
在邏輯層的實體類中,應該像如下示例這樣去管理預期異常:
public static int GetNormalSearchCount(int userID, params object[] parameters) { MvcBasicSiteEntities dataContext = null; // try { dataContext = new MvcBasicSiteEntities(); return dataContext.ExecuteStoreCommand("GetNormalSearchCount", parameters); } catch (System.Data.SqlClient.SqlException exception) { // // Manage the SQL expected exception by generating a MvcBasicException // with more info added to the orginal exception. // throw new MvcBasicException(string.Format( "GetNormalSearchCount for user: {0}", userID), exception); } finally { // // Dispose the used resource. // if(dataContext != null) dataContext.Dispose(); } }
從上面代碼中得知:我們正試圖調用參數化的SQL命令,因為訪問數據庫可能產生異常,並且要保證在任何可能的情況下使用完數據庫連接后必須進行釋放。所以我使用try…catch…finally…結構:
1) 在try塊中,我創建數據實體上下文對象用來訪問數據庫,然后使用數據實體上下文對象來調用SQL命令。
2) 在catch塊中,我僅僅捕獲我想管理的預期異常。在本例子中只捕獲SqlException異常。然后,我通過創建一個MvcBasicException類型的對象來處理該異常,這個對象會包含當前用戶的標識,並且還會將原始的SQLException異常信息保存在內部。最后,我將包含所有所需數據的MvcBasicException異常對象拋出到應用程序更高層。
3) 在finally塊中釋放被使用的資源。在本例中,我在此釋放用於訪問數據庫的數據實體上下文對象。注意,在釋放該對象之前,我先測試該對象是否為null,因為數據實體上下文對象的構造函數肯能會產生SQLException異常,這時該對象並未被成功創建。
用戶界面層預期異常的管理:針對上例邏輯層拋出的MvcBasicException異常,我們在AccountController控制器中處理。
public ActionResult TestExpectedException() { SiteSession siteSession = this.CurrentSiteSession; // try { // // Invoke a method that could generate an exception! // int count = MvcBasic.Logic.User.GetNormalSearchCount( siteSession.UserID, new object[] { "al*", "231" }); // // TO DO! //... } catch (MvcBasicException ex) { MvcBasicLog.LogException(ex); ModelState.AddModelError("", Resources.Resource.ErrorLoadingData); } // // Stay in MyAcount page. // return View("MyAccount"); }
從上面代碼中,我們能得知如何在用戶界面層處理預期異常。首先在try塊中調用可能產生預期異常的邏輯方法,然后在catch塊中捕獲類型為MvcBasicException的預期異常並處理。在用戶界面層使用下面操作處理預期異常:
1) 保存異常數據(消息和堆棧跟蹤)到“事件日志”中。
2) 給用戶在用戶界面顯示一個錯誤消息通知,這個消息內容使用資源文件中的文本以支持多語言。
在_Header.cshtml(頁頭)部分視圖中向用戶顯示錯誤信息(被用於_Layout布局視圖),我添加了一個ValidationSummary類型的對象(也可使用:Label控件、錯誤信息頁面、彈出消息框等其他方式)。
<div class="headerTitle"> @Resources.Resource.HeaderTitle </div> @if (!(Model is LogOnModel)) { <div class="errorMessage"> @Html.ValidationSummary(true) </div> }
注意在一個視圖中應該只存在一個ValidationSummary對象(除非錯誤消息要被多次顯示)。所以在上面代碼中使用if條件判斷來避免為已經存在ValidationSummary對象的LogOn.cshtml頁面顯示驗證信息。
我們將使用已存在的用戶憑證登陸到MVC基礎網站中來進行測試,比如用戶名:Ana,密碼:ana。登陸后,你將看到“My Account”菜單和下面頁面:
現在如果你點擊上面頁面的“Test Expected Exception”鏈接,預期異常將被成功處理並且會在頁頭顯示錯誤信息,就像下圖:
此時,如果你打開Windows的事件查看器,在Application節點中會看到一個來自於示例網站的新條目,如下圖:
如你所見,在“事件日志”中,異常消息和堆棧跟蹤都會被保存,包括我們附加的數據(當前用戶ID)、原始異常信息和產生異常的代碼行。所有這些信息日后能被用於報表分析和解決問題。
未經授權訪問異常管理
未經授權訪問異常:發生在當用戶嘗試訪問一個需要授權或身份驗證的頁面或操作時,而自身未擁有相應的權限。
所有未經授權訪問異常的管理都在BaseConroller基類控制器中,通過重寫OnException方法(該方法繼承自MVC 框架的Controller類)。
protected override void OnException(ExceptionContext filterContext) { if (filterContext.Exception is UnauthorizedAccessException) { // // Manage the Unauthorized Access exceptions // by redirecting the user to Home page. // filterContext.ExceptionHandled = true; filterContext.Result = RedirectToAction("Home", "Index"); } // base.OnException(filterContext); }
如你所見,我只是將用戶重定向到home頁面來簡單的處理異常。
注意,在OnException方法中,其它特定異常也能使用與未經授權訪問異常相同的方式過濾和處理。
在ASP.NET MVC中未經授權訪問站點功能的管理應該使用[Authorize]特性。因此,在所有Controller控制器公開的操作如果需要身份驗證,則必須標注[Authorize]特性,就像下面例子:
[Authorize] public ActionResult MyAccount() { // TO DO! return View(); }
現在測試一下,運行MVC示例程序,不要登陸,然后嘗試使用URL:http://localhost:50646/Account/MyAccount ,來訪問MyAccount操作。
注意,因為這個操作需要身份驗證,所以當前訪問將被重定向到LogOn頁面。因為在Web.config文件中有如下設置:
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
未處理異常管理
未處理異常:是應用程序產生的異常但沒有被當作預期異常在代碼中進行處理的所有異常,當然也包括程序bugs(錯誤和問題)。
為了能管理未處理異常,必須在站點的web.config配置文件中添加或修改下面配置。
<customErrors mode="On"/>
這樣設置后,當拋出一個未處理異常時,ASP.NET MVC框架將激活Error.cshtml頁面。所以管理所有未處理異常的代碼必須寫在Error視圖中。
@using MvcBasic.Logic @using MvcBasicSite.Models @model System.Web.Mvc.HandleErrorInfo @{ ViewBag.Title = "Error"; // // Log out the user and clear its cache. // SiteSession.LogOff(this.Session); // // Log the exception. // MvcBasicLog.LogException(Model.Exception); } <meta http-equiv="refresh" content="5;url=/Home/Index/" /> <h2>@Resources.Resource.ErrorPageMessage</h2>
在上面代碼,未處理異常管理包含四個操作:
1) 調用LogOff方法,退出當前用戶的登陸狀態。
public static void LogOff(HttpSessionStateBase httpSession) { // // Write in the event log the message about the user's Log Off. // Note that could be situations that this code was invoked from Error page // after the current user session has expired, or before the user to login! // SiteSession siteSession = (httpSession["SiteSession"] == null ? null : (SiteSession)httpSession["SiteSession"]); if(siteSession != null) MvcBasicLog.LogMessage(string.Format("LogOn for user: {0}", siteSession.Username)); // // Log Off the curent user and clear its site session cache. // FormsAuthentication.SignOut(); httpSession["SiteSession"] = null; }
2) 使用MvcBasicLog類將異常數據寫到事件日志服務中。
3) 在頁面上顯示一個通用的錯誤消息(從資源文件中獲取)給用戶。
4) 5秒鍾后自動將用戶重定向到Home頁面。
為了驗證這幾點,使用已存在的用戶憑證登陸到MVC示例站點,然后訪問“My Account”菜單,將顯示下面頁面:
現在,如果你點擊頁面上的“Test Unhandled Exception”鏈接,AccountController控制器的TestUnhandledException將產生一個未處理異常。(找不到TestUnhandledException.cshtml視圖)
public ActionResult TestUnhandledException() { // // Next line of code will try to open an view that does not exist ==> Exception. // return View(); }
這時,未處理異常會被Error視圖中的代碼處理,用戶將會退出登陸狀態,並且錯誤消息被顯示到Error頁面上。
5秒后,用戶會自動被重定向到站點的home頁面。
注意,異常數據將被保存到事件日志服務中。如果你打開事件查看器在 Windows 日志中你會在Application節點下看到一個新的來自MVC示例站點的錯誤條目。
通過分析“事件日志”中的錯誤消息,你能得知異常的消息信息和產生異常的源代碼行。(本例中,異常是訪問了一個不存在的視圖頁面)
從ASP.NET MVC3升級到ASP.NET MVC4
為了將解決方案從ASP.NET MVC3升級到ASP.NET MVC4,我按照“MVC4 release notes”說明手動進行升級。
升級之后,我遇到下面兩個問題:(“最新版”都截止於ASP.NET MVC4)
1) 在jquery.unobtrusive-ajax.min.js.最新版腳本中存在一些錯誤。
為了解決這個問題,我創建了一個新的ASP.NET MVC4的項目,並且將生成的最新版jQuery庫覆蓋到手動升級至MVC4的舊項目相應文件。
2) BaseController類中受保護方法ExecuteCore()方法在ASP.NET MVC4框架中不會在每次回發時自動被調用,所以改變當前用戶語言環境功能將失效。
為了解決這個問題,在BaseController類中重寫DisableAsyncSupport屬性並返回true,如下:
protected override bool DisableAsyncSupport { get { return true; } }
升級到ASP.NET MVC4后的網站我也打包在博文開頭處提供了下載鏈接。
本文翻譯到此結束,如果喜歡本系列翻譯分享,還請多幫推薦!!!
本文主要介紹了:通用的異常規則,如何將消息和異常寫入windows的事件日志服務,如何處理預期異常,如何處理未經授權訪問異常,如何處理未處理異常,如何將ASP.NET MVC3升級到ASP.NET MVC4
相關文章:
Upgrading an ASP.NET MVC 3 Project to ASP.NET MVC 4
How to Upgrade an ASP.NET MVC 4 and Web API Project to ASP.NET MVC 5 and Web API 2
原文:MVC Basic Site: Step 2 - Exceptions Management
作者:Raul Iloc