[译]MVC网站教程(二):异常管理

 

介绍

MVC网站教程”系列的目的是教你如何使用 ASP.NET MVC 创建一个基本的、可扩展的网站。

1)   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个小时)

2)        异常管理MVC3—示例源代码.zip

3)        异常管理MVC4—示例源代码.zip

4)        异常管理—数据库bak.zip

 

MVC网站中的异常管理规则

       在每个软件项目中,在项目开发的开始阶段,团队应该定义软件开发中必须遵守的规则。这些规则中应该包含异常管理规则。

       在本篇中,我将描述示例MVC解决方案中使用的异常管理规则,并且这些规则可以作为实际项目的参考。能在任何ASP.NET网站中被重用(或经过比较小的改动适用),而且可以用重用到任何.NET项目中。

       一般的异常管理机制都是使用trycatchfinally…或者using语句来管理执行可能会失败的操作(如:访问数据库表、从文件系统访问文件、使用内存分配、发送电子邮件等等),以合理的方式处理失败并且释放被占用的资源。注意,.NET Framework框架、第三方类库、在程序代码中使用throw关键字都能产生异常。

 

       在示例MVC网站中使用的异常管理规则有:

1.        使用finally块来释放各种不可释放的资源,这些资源是没有实现IDisposable接口的但是又需要一些释放操作,比如:close()掉在try块中打开的Stream或文件。

2.        访问可释放资源时可能会产生异常,不要忘记使用trycatchfinally…或using语句来释放占用的资源。

3.        不要在不需要的地方使用trycatch…吞掉异常,可以考虑使用ifelse…语句避免异常发生从而提高程序性能。比如,在可能为空的对象上执行任何操作之前做非空判断,能显著的提高应用程序性能。

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服务。

     [译]MVC网站教程(二):异常管理_第1张图片

如上面类图中所见,这个类包含了一系列公共的静态方法用于在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日志”)

      [译]MVC网站教程(二):异常管理_第2张图片

注意示例中的LogException方法,会将整个异常数据都写入事件日志中,结果如下图:

  [译]MVC网站教程(二):异常管理_第3张图片

       从上图,你能看到事件日志的详细异常消息、异常堆栈跟踪以及产生异常的源代码行。

       所以,在程序使用期间产生的每个“错误”都会记录在事件日志中,开发人员可以非常容易的确认错误产生的代码行以及问题的上下文环境。

 

预期异常管理

在源代码中,当你实现的一些操作可能存在失败的情况下,比如访问数据库表、从文件系统中访问文件、分配内存、发送电子邮件等等,这些操作会产生预期的异常。同样,在实现的逻辑代码中如果违反应用程序的逻辑规则时(比如,对未授权的页面进行访问),也会抛出预期异常。

       预期异常是异常管理过程中重要的部分。为了实现该功能,我们创建一个适当的继承自特定的异常基类的异常类。

       异常管理过程中其他比较重要的方面是:许多预期异常,产生自原代码的底层,关于这个问题的消息通知应该被显示到用户界面层,所以原始异常的消息必须一层一层的传递直到在用户界面层被处理。在传递异常数据的过程中,会积累越来越多的上下文信息,这些信息将用于描述和定位问题,并且全部保存在日志中,以便将来使用(如:日志报表和解决问题)。

       我使用MvcBasicException基类来管理预期异常。这个类继承自ApplicationException基类,并且标注[Serializable]特性以支持序列化。

注意:序列化是经常发生的,但是却常常不为人知。序列化机制是所有跨应用程序域调用的基础,甚至还会发生在同一个进程内,比如从逻辑层传递数据到表现层。因此,为了保存和在各层之间传递整个异常数据(异常信息和堆栈跟踪),我们必须为我们的异常类标注[Serializable]特性,同时还必须为支持反序列化提供一个受保护的构造函数。

     [译]MVC网站教程(二):异常管理_第4张图片

       如类图中所见,这个类有三个构造函数,被用于以下场景:

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命令,因为访问数据库可能产生异常,并且要保证在任何可能的情况下使用完数据库连接后必须进行释放。所以我使用trycatchfinally…结构:

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”菜单和下面页面:

      [译]MVC网站教程(二):异常管理_第5张图片

现在如果你点击上面页面的“Test Expected Exception”链接,预期异常将被成功处理并且会在页头显示错误信息,就像下图:

[译]MVC网站教程(二):异常管理_第6张图片

此时,如果你打开Windows的事件查看器,在Application节点中会看到一个来自于示例网站的新条目,如下图:

[译]MVC网站教程(二):异常管理_第7张图片

如你所见,在“事件日志”中,异常消息和堆栈跟踪都会被保存,包括我们附加的数据(当前用户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示例程序,不要登陆,然后尝试使用URLhttp://localhost:50646/Account/MyAccount ,来访问MyAccount操作。

       注意,因为这个操作需要身份验证,所以当前访问将被重定向到LogOn页面。因为在Web.config文件中有如下设置:

    <authentication mode="Forms">
      <forms loginUrl="~/Account/LogOn" timeout="2880" />
    </authentication>

      [译]MVC网站教程(二):异常管理_第8张图片

 

未处理异常管理

未处理异常:是应用程序产生的异常但没有被当作预期异常在代码中进行处理的所有异常,当然也包括程序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”菜单,将显示下面页面:

[译]MVC网站教程(二):异常管理_第9张图片

 

       现在,如果你点击页面上的“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页面上。

     [译]MVC网站教程(二):异常管理_第10张图片

       5秒后,用户会自动被重定向到站点的home页面。

注意,异常数据将被保存到事件日志服务中。如果你打开事件查看器在 Windows 日志中你会在Application节点下看到一个新的来自MVC示例站点的错误条目。

[译]MVC网站教程(二):异常管理_第11张图片

通过分析“事件日志”中的错误消息,你能得知异常的消息信息和产生异常的源代码行。(本例中,异常是访问了一个不存在的视图页面)

 

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 

 

你可能感兴趣的:([译]MVC网站教程(二):异常管理)