65.9K
CodeProject 正在变化。 阅读更多。
Home

基础回归:N层 ASP.NET 应用程序异常管理设计指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (25投票s)

2011年2月9日

CPOL

13分钟阅读

viewsIcon

52328

努力为 N 层 ASP.NET 应用程序制定基本的异常管理设计指南

引言

“您如何为 N 层 ASP.NET 应用程序定义良好的异常管理?”

一个很基本的问题,但答案并非那么简单。

我们擅长制造事物。但是,也许我们并不同样擅长设计一个能够优雅地正确处理错误、向用户提供关于错误的友好提示且不让用户陷入僵局,并在内部向系统开发人员提供足够详细信息以便开发人员不必学习某种火箭科学就能修复这些错误。

所以,如果您问我同样的问题,我会告诉您以下内容:

当发生错误时,您的系统拥有良好的异常管理,如果:

  • 它不会显示不必要的错误技术描述,而是向用户道歉,显示一个“发生了一些错误”的屏幕,并允许用户返回系统。
  • 当发生错误时,它会立即通知技术团队,提供详细的故障排除信息,并记录错误详情。
  • 它以集中且可管理的方式进行异常管理,而不会在整个代码库中散布不必要的 try..catch...throw

因此,如果我们想确保我们的 ASP.NET 应用程序拥有良好的异常管理,我们需要达到这三个高层目标。

您应该做的最基本的事情

如果您是世界上最懒惰的开发人员(就像我几年前一样),您至少应该利用 ASP.NET 提供的功能来优雅地处理异常。您只需要执行以下两个简单步骤:

在 web.config 中启用 customError

<customerrors defaultredirect="Error.aspx" mode="On">
</customerrors>

您可能已经知道,这个小小的配置指示 ASP.NET 运行时在您的 ASP.NET 应用程序中发生错误时重定向到 Error.aspx。设置 mode="On" 表示始终重定向,这对于您开发系统来说可能不是一个好选择。设置 mode="RemoteOnly" 应该是您的完美选择,因为它只会在页面从远程计算机浏览时重定向到错误页面。

定义错误页面

您当然需要创建一个错误页面,对吧?您可能想要的 Error.aspx 页面如下:

ErrorPage.png

图:ASP.NET 中最简单的错误页面

因此,我使用 Visual Studio 2010 创建的 ASP.NET 网站中的 Error.aspx 如下:

标记

<@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" 
	AutoEventWireup="true" CodeFile="Error.aspx.cs" Inherits="Error" >
<asp:content contentplaceholderid="HeadContent" id="HeaderContent" runat="server">
</asp:content>
<asp:content contentplaceholderid="MainContent" id="BodyContent" runat="server">
  <asp:panel id="pnlError" runat="server" visible="false">
    <asp:label id="lblError" runat="server" text="Oops! 
	An error occurred while performing your request. 
	Sorry for any convenience."></asp:label>
    <asp:label id="lblGoBack" runat="server" 
	text="You may want to get back to the previous page 
	and perform other activities."></asp:label>
    <asp:hyperlink id="hlinkPreviousPage" runat="server">Go back</asp:hyperlink>
  </asp:panel>
</asp:content>

CodeBehind

public partial class Error : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    Page.Title = "Error occurred";
    string PreviousUri = Request["aspxerrorpath"];
    if (!string.IsNullOrEmpty(PreviousUri))
    {
      pnlError.Visible = true;
      hlinkPreviousPage.NavigateUrl = PreviousUri;
    }
  }
}

执行这两个步骤后,每当发生未处理的异常时,ASP.NET 运行时都会将当前请求重定向到错误页面(在本例中为 Error.aspx),并带有一个查询 string 参数“aspxerrorpath”,其值设置为上一个页面的 URL。因此,Error.aspx.cs 能够从该请求参数读取上一个页面的 URL,并显示一个友好的错误消息,其中包含一个“返回”超链接,让用户返回到上一页。

为了测试它是否有效,我创建了一个异常场景,如下所示:

  • 用户浏览 Default.aspx 并点击“执行操作”按钮。
  • 按钮点击事件处理程序方法抛出异常。
  • ASP.NET 运行时捕获此异常并重定向用户到错误页面。

结果正如预期的那样,运行良好。

谁来修复错误?

非常重要的问题。您的系统现在能够优雅地处理异常了,这很好。但这还不够,对吧?我们需要一种机制来捕获异常并通知我们的技术团队,或者将异常详细信息记录在某个地方,以便他们能够尽快分析和修复错误。

因此,我们需要一种方法来捕获异常并做一些我们自己的事情。让我们看看如何做到这一点。

CodeBehind 类上的 Page.OnError() 事件方法

每个 ASP.NET Page CodeBehind 类都继承了 System.Web.UI.Page 类,并且可以覆盖基类的 OnError() 事件。这样做可以让页面使用以下代码捕获任何未处理的异常:

protected override void OnError(EventArgs e)
{
  Response.Redirect(string.Format
	("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}

由于我们的 ASP.NET 应用程序通常有多个页面,因此在所有 CodeBehind 页面的一个基类(例如 BasePage)中定义 OnError() 会更明智,如下所示:

public class BasePage : System.Web.UI.Page
{
  public BasePage()
  {
    //
    // TODO: Add constructor logic here
    //
  }
  protected override void OnError(EventArgs e)
  {
    //Report Error
    Exception ex = Server.GetLastError();
    ErrorHandler.ReportError(ex);
    Server.ClearError();
    Response.Redirect(string.Format
	("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
  }
}

这将使我们不必在每一个 CodeBehind 页面上实现 OnError 事件方法。

Global.asax 中的 Application_Error() 事件

您可能已经知道,您可以在 Global.asax 文件中定义应用程序级别的事件,这允许您定义一个全局错误处理事件方法 Application_Error()。如果您在此文件中定义了 Application_Error() 事件方法,那么每一个未处理的异常都将被此事件方法捕获,您可以随心所欲。在我们的情况下,主要是在此事件方法中报告异常详细信息,同时将用户重定向到错误页面:

void Application_Error(object sender, EventArgs e)
{
  // Code that runs when an unhandled error occurs
  //Report Error
  Exception ex = Server.GetLastError();
  ErrorHandler.ReportError(ex); //Notifies technical team about the error
  Server.ClearError();
  Response.Redirect(string.Format
	("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}

如果您通过 Global.asax 中的 Application_Error 事件处理程序处理异常,则无需通过 Page.OnError() 事件处理程序处理异常。因此,事情变得更加轻松。

异常不应该在 CodeBehind 类中处理吗?

这是一个好问题。当然可以建议在 CodeBehind 类中处理异常,如下所示:

try
{
  //Do some stuff
  businessObject.SaveObject(object);
}
catch(Exception ex)
{
  ErrorHandler.ReportError(ex); //Notifies technical team about the error
  Response.Redirect(string.Format
	("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}

但是,CodeBehind 类可能包含许多代码,这些代码可以调用其他层的方法,并且可能会从这些方法中抛出异常。此外,会有很多 CodeBehind 类。因此,以上述方式,try...catch 异常处理代码必须在数百个地方编写(不要忘记,如果我们要在异常处理机制中进行更改,我们还需要在数百个地方更改代码)。当我们可以从一个中心位置(Application_Error())处理异常时,这真的听起来像是一个明智的做法吗?绝对不是。

因此,请不要在 CodeBehind 类中编写任何 try...catch 块,并使其完全不受任何丑陋的错误处理机制的影响。(好吧,这个建议可能有一个例外,我们稍后会看到。)利用 ASP.NET 的强大功能,让您的异常处理机制保持简单且可从中心位置管理。

我有一个 N 层 ASP.NET 应用程序。我应该如何处理异常?

这是一个经典问题。多层应用程序现在非常普遍,在这些应用程序中应该遵循什么样的异常处理策略?我们应该使用 try...catch 块在每一层中处理异常吗?我们应该将异常从一层抛到上一层吗?我们应该在每一层中记录异常吗?

我会尝试将答案简化如下。您应该做任何必要的事情来满足您的目标。也就是说,向技术团队报告错误并向最终用户提供友好的异常处理。您还需要确保您没有在多个层中编写相同的重复代码来在多个地方处理异常。

因此,让我们通过分析我们的高层目标并运用一些常识,为 N 层 ASP.NET 应用程序制定一个异常处理策略。

我们需要优雅地处理异常

我们的 ASP.NET 应用程序有多少层并不重要。只要我们在 Global.asax 中定义了一个 Application_Error 事件,就没有什么能逃脱它。因此,无论异常在我们多层应用程序的何处发生,异常都将在 Global.asax 中被捕获,它将执行它被指示执行的操作(将用户重定向到错误页面)。
我们需要将异常详细信息通知技术团队。

是的,Application_Error 事件方法也通过某种机制向技术团队报告异常详细信息。但是,请稍等!我们真的需要通知技术团队每一个异常吗?可能不需要。
我们需要向技术团队报告哪些异常?

假设,在我们的应用程序中的数据访问层(一个单独的 DLL)中调用了一个数据访问层方法,并且 DAL 方法可能会从数据库抛出异常。我们应该将这些来自数据访问层的异常报告给技术团队吗?显然是的。为什么?因为错误需要在数据库例程中修复。

场景 1

如果任何未处理的异常(例如 NullReferenceException)从代码的任何部分抛出(无论是从 CodeBehind 类内部生成的,还是从任何其他层生成的),我们应该报告给技术团队吗?显然是的。为什么?出于同样的原因,错误需要在代码中通过一些良好的防御性编程来修复。

场景 2

假设我们从用户那里获取输入,并使用输入参数调用一个业务方法。业务方法期望某个参数值在某个范围内。如果它不在该范围内,它会抛出一个带有适当消息的异常。我们应该将这类异常报告给技术团队吗?显然不是。为什么?因为这种异常通常是故意的,目的是让用户知道他们做错了什么,并且异常消息用于指导他们做正确的事情。

场景 3

假设在使用 CodeBehind 类中的 Response.Redirect() 时生成了 ThreadAbortException。在 Application_Error 事件处理程序中捕获此异常后,我们是否应该通知开发人员?显然不是。为什么?因为这个特定的异常不是由程序性或系统性故障生成的,而且没有什么需要修复的。

因此,在多层应用程序中,异常可能由每一层生成,并且这些异常可能属于不同的通用类别,并且基于它们的类别,这些异常需要以不同的方式处理。某些特定类别的异常需要报告给某些特定的技术团队,某些特定类别的异常不需要报告给任何技术团队,而是用于指导用户正确使用系统,而某些特定类别的异常只需要被忽略。因此,这产生了一个需要开发一个可以在应用程序中的所有不同层中通用的异常层级结构的需求。

每一层生成的异常都将根据其相应的通用类型进行分类(当发生异常时,这将通过将异常包装在异常层级结构中的特定类型异常中来完成),并将抛出到上一层,以便异常管理策略能够确定异常的类别并明智地处理它们。

自定义异常层级结构

以下可能是一个非常基本的异常层级结构,可用于包装原始异常并对其进行分类,以便使用良好的异常管理策略来处理这些异常。如果我们有一个数据访问层和一个业务层(两个独立的类库)的应用程序,则可以使用以下自定义异常层级结构:

CustomExceptionHierarchy.png

图:N 层应用程序的基本自定义异常层级结构

Exception 类名称不言自明。它们都继承了一个基异常类 BaseException,该类本身继承了 Exception 类。

因此,此 Exception 层级结构可以如下使用:

在 DAL 方法内部

捕获调用数据库操作可能抛出的任何 Exception,将其包装在 DALException 中,然后抛出到上一层。

public List<User> GetUsers()
{
  string SP = AppConstants.StoredProcedures.GET_ALL_USERS;
  IDataReader reader = null;
  List<User> users = new List<User>();
  try
  {
    using (DbCommand dbCommand = Database.GetStoredProcCommand(SP))
    {
      using (reader = Database.ExecuteReader(dbCommand))
      {
        while (reader.Read())
        {
          User user = User.CreateFromReader(reader);
          users.Add(user);
        }
      }
    }
  }
  catch(Exception ex)
  {
    throw new DALException("Failed to retrieve User.", ex);
  }
  return users;
}

在业务方法内部

通常,在调用此(或任何)DAL 方法时,我们不需要在业务方法内部处理异常。为什么?因为 DAL 方法已经处理了异常。因此,当调用业务方法中的 DAL 方法时发生任何异常时,异常将通过上一层传播到调用层级结构,并被全局异常处理程序 Application_Error() 捕获。

因此,您只需在业务方法中的一个方法中调用 DAL 方法,如下所示:

public List<User> GetUsers()
{
  UserDAO userDAO = new UserDAO();
  List<User> users = userDAO.GetUsers();
  if(users != null)
  {
    //Do other stuffs
  }
  //Do other stuffs
  return users;
}

但是,如前所述,我们可能需要在某些业务方法中抛出一些自定义异常,如下所示(以让调用者知道必须满足某个条件才能继续操作):

public void Add(User user)
{
  if(user.Age > 18)
  {
    throw new BLLException("Not allowed for kids!");
  }
  UserDAO userDAO = new UserDAO();
  userDAO.Add(user);
}

在 CodeBehind 类内部

只有 BLLException 类型的异常需要在这里处理。为什么?因为 BLLException 实际上代表了一个业务逻辑验证错误(带有验证错误消息,请参见上面的示例),需要显示给最终用户,以指导他们提供正确的输入或正确执行他们的操作。如果我们不在 CodeBehind 类中处理 BLLException,这些异常将传播到全局错误处理程序 Application_Error,它将要么忽略该异常,要么将异常报告给技术团队,这两种情况都不是期望的结果。

因此,以下是我们如何在 CodeBehind 类中处理 BLLExceptions

protected bool AddUser()
{
  UserManager userManager = new UserManager();
  User user = PopulateUserFromInput();
  try
  {
    userManager.Add(user);
  }
  catch(BLLException ex)
  {
    ShowErrorMessage(ex.Message);
    return false;
  }
  return true;
}

在 Global.asax 的 Application_Error() 事件中

当异常冒泡到此全局异常处理方法时,它可能是 ThreadAbortException,或者 DALException,或者 BLLException,或者一个未处理的异常,我们没有在应用程序中使用任何 try...catch 块来捕获它。

Application_Error 事件处理方法将只对 DALException 和任何未处理的异常感兴趣。如我们所知,它将报告异常详细信息(可能通过向预配置的电子邮件地址发送电子邮件,并将异常详细信息记录在某个地方)。它将简单地忽略 ThreadAbortExceptions(因为没有什么需要修复的)和 BLLException(因为这实际上是一个自定义异常,应该在 CodeBehind 类中处理,以向用户显示验证消息)。

Application_Error 事件方法可能如下所示:

void Application_Error(object sender, EventArgs e)
{
  Exception ex = Server.GetLastError();
  if (ex as System.Threading.ThreadAbortException != null && 
			ex as BLLException != null)
  {
    //Do not handle any ThreadAbortException or BLLException here
    //Only handle other custom exception (DALException in this example)
    //and any other unhandled Exceptions here
    ErrorHandler.ReportError(ex);
    Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}", 
			Request.Url.PathAndQuery));
  }
}

ASP.NET 和 Microsoft 异常处理应用程序块

您可以使用 Microsoft 提供的异常处理应用程序块,它可以完美地满足您的需求。您可以以声明方式为每种类型的异常定义异常策略,并且您可以配置该块如何处理每种类型的异常。安装后,您可以在 Visual Studio 中使用 GUI 工具配置应用程序块。

此链接可以帮助您在 ASP.NET 应用程序中配置异常处理应用程序块。我觉得它很有用。

结论

在设计 N 层 ASP.NET 应用程序的异常处理机制时,请记住以下简单规则:

  • 定义一个全局异常处理程序,通过异常报告机制(可能通过发送电子邮件和记录异常)将异常详细信息报告给技术团队,并将用户重定向到一个错误页面,提供一个友好的消息并提供一个返回原始页面的链接。
  • 使用适合您需求的异常层级结构对异常进行分类,并定义一个经过深思熟虑的策略来处理每一类异常。
  • 让所有异常传播到全局异常处理程序,除了自定义异常(在此示例中为 BLLException),这些异常旨在向用户显示错误消息,以指导他们正确完成工作。
  • 一旦从 N 层应用程序中的任何层抛出异常(无论是您抛出的还是 CLR 抛出的),只在单个位置(无论是 CodeBehind 类还是全局异常处理程序)catch 它,并在捕获后不要重新抛出。Throw 一次,Catch 一次。

请记住,计划先于行动。在 N 层 ASP.NET 应用程序中构建一个清晰、经过深思熟虑的异常处理机制,将使您的系统平稳运行,并在错误发生时立即通知您需要修复的错误。最终结果是,您的开发团队、管理层和客户,每个人都满意。

历史

  • 2011 年 2 月 9 日:初始帖子
© . All rights reserved.