处理未经处理的异常 (C#)
作者 :斯科特·米切尔
当生产环境中 Web 应用程序发生运行时错误时,请务必通知开发人员并记录错误,以便在以后的某个时间点诊断它。 本教程概述了如何 ASP.NET 处理运行时错误,并查看一种方法,让自定义代码在未经处理的异常气泡到 ASP.NET 运行时时执行。
简介
在 ASP.NET 应用程序中发生未经处理的异常时,它会弹出到 ASP.NET 运行时,这会引发 Error
事件并显示相应的错误页。 有三种不同类型的错误页:运行时错误黄色屏幕死亡(YSOD):异常详细信息 YSOD;和自定义错误页。 在前面的教程中,我们配置了应用程序,以便为远程用户使用自定义错误页,并为本地访问的用户配置异常详细信息 YSOD。
使用与网站外观匹配的用户友好自定义错误页首选为默认运行时错误 YSOD,但显示自定义错误页只是综合错误处理解决方案的一部分。 在生产中的应用程序中发生错误时,开发人员必须收到错误通知,以便他们能够发掘异常的原因并解决该错误。 此外,应记录错误的详细信息,以便在以后的时间点检查和诊断错误。
本教程介绍如何访问未经处理的异常的详细信息,以便可以记录这些异常并通知开发人员。 以下两个教程探讨了错误日志记录库,这些库经过一些配置后,会自动通知开发人员运行时错误并记录其详细信息。
注意
如果需要以某种唯一或自定义的方式处理未经处理的异常,本教程中介绍的信息最有用。 如果只需要记录异常并通知开发人员,使用错误日志记录库就是一种方法。 接下来的两个教程概述了两个此类库。
引发事件时Error
执行代码
事件为对象提供一种机制,用于指示发生了一些有趣的内容,以及另一个对象在响应中执行代码。 作为 ASP.NET 开发人员,你习惯于在事件方面思考。 如果要在访问者单击特定按钮时运行某些代码,请为该 Button Click
的事件创建事件处理程序,并将代码放在其中。 鉴于每当发生未经处理的异常时,ASP.NET 运行时都会引发其 Error
事件 ,因此记录错误详细信息的代码将出现在事件处理程序中。 但是如何为 Error
事件创建事件处理程序?
该Error
事件是在请求生存期内在 HTTP 管道的某些阶段引发的类中的HttpApplication
许多事件之一。 例如,HttpApplication
类的事件BeginRequest
在每个请求的开头引发;当安全模块标识请求者时,将引发该AuthenticateRequest
事件。 这些 HttpApplication
事件为页面开发人员提供了在请求生存期内的各个点执行自定义逻辑的方法。
事件的 HttpApplication
事件处理程序可以放置在名为 Global.asax
的特殊文件中。 若要在网站中创建此文件,请使用名称 Global.asax
为“全局应用程序类”模板将新项添加到网站的根目录中。
图 1:添加到 Global.asax
Web 应用程序
(单击可查看全尺寸图像)
Visual Studio 创建的文件的内容和结构 Global.asax
因使用的是 Web 应用程序项目(WAP)还是网站项目(WSP)而略有不同。 使用 WAP,将 Global.asax
实现为两个单独的文件 - Global.asax
和 Global.asax.cs
。 该文件 Global.asax
只 @Application
包含引用文件的 .cs
指令;相关事件处理程序在 Global.asax.cs
文件中定义。 对于 WSP,只创建单个文件, Global.asax
事件处理程序在块中 <script runat="server">
定义。
Global.asax
由 Visual Studio 的全局应用程序类模板在 WAP 中创建的文件包括名为 Application_BeginRequest
Application_Error
分别作为事件的BeginRequest
AuthenticateRequest
事件处理程序HttpApplication
,以及Error
。 还有名为 Application_Start
Session_Start
Application_End
的事件处理程序,以及Session_End
当 Web 应用程序启动时、新会话启动时、应用程序结束和会话结束时分别触发的事件处理程序。 Global.asax
Visual Studio 在 WSP 中创建的文件仅Session_End
Application_Error
Application_Start
Session_Start
Application_End
包含事件处理程序和事件处理程序。
注意
部署 ASP.NET 应用程序时,需要将 Global.asax
文件复制到生产环境。 Global.asax.cs
在 WAP 中创建的文件不需要复制到生产环境,因为此代码编译到项目的程序集中。
Visual Studio 的全局应用程序类模板创建的事件处理程序并不详尽。 可以通过命名事件处理程序来为任何 HttpApplication
事件添加事件处理程序 Application_EventName
。 例如,可以将以下代码添加到 Global.asax
文件中,为 AuthorizeRequest
事件创建事件处理程序:
protected void Application_AuthorizeRequest(object sender, EventArgs e)
{
// Event handler code
}
同样,可以删除由不需要的全局应用程序类模板创建的任何事件处理程序。 在本教程中,我们只需要事件的 Error
事件处理程序;可以随意从 Global.asax
文件中删除其他事件处理程序。
注意
HTTP 模块 提供另一种方法来定义事件的事件处理程序 HttpApplication
。 HTTP 模块是作为类文件创建的,可以直接放置在 Web 应用程序项目中,也可以分隔成单独的类库。 由于这些模块可以分离到类库中,因此 HTTP 模块提供了一个更灵活且可重用的模型,用于创建 HttpApplication
事件处理程序。 Global.asax
虽然该文件特定于驻留的 Web 应用程序,但 HTTP 模块可以编译为程序集,此时将 HTTP 模块添加到网站非常简单,只需在文件夹中删除程序集Bin
并注册模块Web.config
。 本教程不介绍如何创建和使用 HTTP 模块,但以下两个教程中使用的两个错误日志记录库作为 HTTP 模块实现。 有关 HTTP 模块的优点的更多背景信息, 请参阅使用 HTTP 模块和处理程序创建可插入 ASP.NET 组件。
检索有关未经处理的异常的信息
此时,我们有一个包含事件处理程序的 Application_Error
Global.asax 文件。 执行此事件处理程序时,我们需要通知开发人员错误并记录其详细信息。 若要完成这些任务,我们首先需要确定引发的异常的详细信息。 使用 Server 对象的 GetLastError
方法 检索导致事件触发 Error
的未经处理的异常的详细信息。
protected void Application_Error(object sender, EventArgs e)
{
// Get the error details
HttpException lastErrorWrapper =
Server.GetLastError() as HttpException;
}
该方法 GetLastError
返回一个类型 Exception
的对象,该对象是 .NET Framework 中所有异常的基类型。 但是,在上面的代码中,我正在将返回的 GetLastError
Exception 对象强制转换为对象 HttpException
。 Error
如果由于在处理 ASP.NET 资源期间引发异常而引发该事件,则会在一个HttpException
内部包装引发的异常。 若要获取引发 Error 事件的实际异常,请使用该 InnerException
属性。 Error
如果由于基于 HTTP 的异常(例如对不存在的页面的请求)而引发该事件,则会引发一个HttpException
事件,但它没有内部异常。
以下代码使用检索GetLastErrormessage
有关触发Error
事件的异常的信息,并将该异常存储在名为 <HttpException
变量中。 然后,它将源异常的类型、消息和堆栈跟踪存储在三个字符串变量中,检查是否是触发Error
事件的实际异常(在基于 HTTP 的异常的情况下),或者它是否lastErrorWrapper
只是处理请求时引发的异常的包装器。
protected void Application_Error(object sender, EventArgs e)
{
// Get the error details
HttpException lastErrorWrapper =
Server.GetLastError() as HttpException;
Exception lastError = lastErrorWrapper;
if (lastErrorWrapper.InnerException != null)
lastError = lastErrorWrapper.InnerException;
string lastErrorTypeName = lastError.GetType().ToString();
string lastErrorMessage = lastError.Message;
string lastErrorStackTrace = lastError.StackTrace;
}
此时,需要编写将异常详细信息记录到数据库表的代码所需的所有信息。 可以创建一个数据库表,其中包含每个感兴趣的错误详细信息(类型、消息、堆栈跟踪等)以及其他有用的信息片段,例如所请求页面的 URL 和当前登录用户的名称。 在事件处理程序中 Application_Error
,将连接到数据库并将记录插入表中。 同样,可以添加代码,以通过电子邮件提醒开发人员出现错误。
在接下来的两个教程中检查的错误日志记录库提供此类功能,因此无需自行生成此错误日志记录和通知。 但是,为了说明 Error
正在引发该事件,并且 Application_Error
事件处理程序可用于记录错误详细信息并通知开发人员,让我们添加代码,在发生错误时通知开发人员。
在发生未经处理的异常时通知开发人员
在生产环境中发生未经处理的异常时,请务必向开发团队发出警报,以便他们可以评估错误并确定需要执行的操作。 例如,如果在连接到数据库时出错,则需要仔细检查连接字符串,并且可能还会向 Web 托管公司开具支持票证。 如果由于编程错误而发生异常,可能需要添加其他代码或验证逻辑,以防止将来出现此类错误。
命名空间中的 System.Net.Mail
.NET Framework 类可以轻松发送电子邮件。 该MailMessage
类表示电子邮件,并具有属性,如To
、From
、Subject
Body
和Attachments
。 用于SmtpClass
使用指定的 SMTP 服务器发送MailMessage
对象;可以在元素Web.config file
中<system.net>
以编程方式或以声明方式指定 SMTP 服务器设置。 有关在 ASP.NET 应用程序中发送电子邮件的详细信息,请查看我的文章:从 ASP.NET 网页站点发送电子邮件和 System.Net.Mail。
注意
该 <system.net>
元素包含类在发送电子邮件时使用的 SmtpClient
SMTP 服务器设置。 Web 托管公司可能具有可用于从应用程序发送电子邮件的 SMTP 服务器。 有关应在 Web 应用程序中使用的 SMTP 服务器设置的信息,请参阅 Web 主机的支持部分。
将以下代码添加到事件处理程序, Application_Error
以在发生错误时向开发人员发送电子邮件:
void Application_Error(object sender, EventArgs e)
{
// Get the error details
HttpException lastErrorWrapper =
Server.GetLastError() as HttpException;
Exception lastError = lastErrorWrapper;
if (lastErrorWrapper.InnerException != null)
lastError = lastErrorWrapper.InnerException;
string lastErrorTypeName = lastError.GetType().ToString();
string lastErrorMessage = lastError.Message;
string lastErrorStackTrace = lastError.StackTrace;
const string ToAddress = "support@example.com";
const string FromAddress = "support@example.com";
const string Subject = "An Error Has Occurred!";
// Create the MailMessage object
MailMessage mm = new MailMessage(FromAddress, ToAddress);
mm.Subject = Subject;
mm.IsBodyHtml = true;
mm.Priority = MailPriority.High;
mm.Body = string.Format(@"
<html>
<body>
<h1>An Error Has Occurred!</h1>
<table cellpadding=""5"" cellspacing=""0"" border=""1"">
<tr>
<tdtext-align: right;font-weight: bold"">URL:</td>
<td>{0}</td>
</tr>
<tr>
<tdtext-align: right;font-weight: bold"">User:</td>
<td>{1}</td>
</tr>
<tr>
<tdtext-align: right;font-weight: bold"">Exception Type:</td>
<td>{2}</td>
</tr>
<tr>
<tdtext-align: right;font-weight: bold"">Message:</td>
<td>{3}</td>
</tr>
<tr>
<tdtext-align: right;font-weight: bold"">Stack Trace:</td>
<td>{4}</td>
</tr>
</table>
</body>
</html>",
Request.RawUrl,
User.Identity.Name,
lastErrorTypeName,
lastErrorMessage,
lastErrorStackTrace.Replace(Environment.NewLine, "<br />"));
// Attach the Yellow Screen of Death for this error
string YSODmarkup = lastErrorWrapper.GetHtmlErrorMessage();
if (!string.IsNullOrEmpty(YSODmarkup))
{
Attachment YSOD =
Attachment.CreateAttachmentFromString(YSODmarkup, "YSOD.htm");
mm.Attachments.Add(YSOD);
}
// Send the email
SmtpClient smtp = new SmtpClient();
smtp.Send(mm);
}
虽然上述代码相当长,但大部分代码会创建发送给开发人员的电子邮件中显示的 HTML。 代码首先引用 HttpException
方法返回 GetLastError
的值(lastErrorWrapper
)。 通过检索 lastErrorWrapper.InnerException
请求引发的实际异常,并分配给变量 lastError
。 从三个字符串变量中 lastError
检索和存储类型、消息和堆栈跟踪信息。
接下来,创建一个名为MailMessage
mm
的对象。 电子邮件正文采用 HTML 格式,并显示所请求页面的 URL、当前登录用户的名称以及有关异常的信息(类型、消息和堆栈跟踪)。 类 HttpException
的一个很酷的事情是,可以通过调用 GetHtmlErrorMessage 方法生成用于创建异常详细信息黄色屏幕(YSOD)的 HTML。 此方法用于检索异常详细信息 YSOD 标记,并将其作为附件添加到电子邮件。 一个警告:如果触发事件的 Error
异常是基于 HTTP 的异常(例如对不存在的页面的请求),则 GetHtmlErrorMessage
该方法将返回 null
。
最后一步是发送 MailMessage
. 这是通过创建新 SmtpClient
方法并调用其方法来完成的 Send
。
注意
在 Web 应用程序中使用此代码之前,需要将常量中的ToAddress
值从support@example.com任何电子邮件地址更改为错误通知电子邮件应发送到和FromAddress
源自的任何电子邮件地址。 还需要在节Web.config
中<system.net>
指定 SMTP 服务器设置。 请查阅 Web 主机提供程序以确定要使用的 SMTP 服务器设置。
每当出现错误时,开发人员都发送了一封电子邮件,用于汇总错误并包含 YSOD。 在前面的教程中,我们演示了运行时错误:访问Genre.aspx并通过查询字符串传入无效 ID
值,例如 Genre.aspx?ID=foo
。 访问具有 Global.asax
文件的页面会产生与上一教程中相同的用户体验 - 在开发环境中,你将继续看到“异常详细信息”黄色死亡屏幕,而在生产环境中,你将看到自定义错误页。 除了此现有行为外,开发人员还会发送电子邮件。
图 2 显示了访问 Genre.aspx?ID=foo
时收到的电子邮件。 电子邮件正文汇总了异常信息,而 YSOD.htm
附件显示异常详细信息 YSOD 中显示的内容(请参阅 图 3)。
图 2:每当出现未经处理的异常时,开发人员都发送电子邮件通知
(单击可查看全尺寸图像)
图 3:电子邮件通知包括异常详细信息 YSOD 作为附件
(单击可查看全尺寸图像)
使用自定义错误页怎么样?
本教程演示了如何在发生未经处理的异常时使用 Global.asax
和 Application_Error
事件处理程序执行代码。 具体而言,我们使用此事件处理程序通知开发人员出现错误;我们可以将其扩展,以记录数据库中的错误详细信息。 事件处理程序的存在 Application_Error
不会影响最终用户的体验。 它们仍会看到配置的错误页,无论是错误详细信息 YSOD、运行时错误 YSOD 还是自定义错误页。
使用自定义错误页时,想知道文件和Application_Error
事件是否Global.asax
是必需的。 发生错误时,用户会显示自定义错误页,因此为什么我们不能将代码通知开发人员并将错误详细信息记录到自定义错误页的代码隐藏类中? 虽然你当然可以将代码添加到自定义错误页的代码隐藏类,但你无权访问使用我们在前面的教程中探索的技术时触发 Error
事件的异常的详细信息。 GetLastError
从自定义错误页调用该方法将Nothing
返回 。
此行为的原因是通过重定向访问自定义错误页。 当未经处理的异常到达 ASP.NET 运行时时,ASP.NET 引擎会引发其Error
事件(该事件执行Application_Error
事件处理程序),然后通过发出一个Response.Redirect(customErrorPageUrl)
命令将用户重定向到自定义错误页。 该方法 Response.Redirect
使用 HTTP 302 状态代码向客户端发送响应,指示浏览器请求新的 URL,即自定义错误页。 然后,浏览器会自动请求此新页面。 你可以判断自定义错误页是单独请求的,因为浏览器的地址栏更改了自定义错误页 URL(请参阅 图 4)。
图 4:发生错误时,浏览器将重定向到自定义错误页 URL
(单击可查看全尺寸图像)
净效果是当服务器响应 HTTP 302 重定向时发生未经处理的异常的请求结束。 对自定义错误页的后续请求是全新的请求;至此,ASP.NET 引擎放弃了错误信息,此外,无法将上一个请求中的未处理的异常与自定义错误页的新请求相关联。 这就是为什么从自定义错误页调用时返回null
的原因GetLastError
。
但是,可以在导致错误的同一请求期间执行自定义错误页。 该方法 Server.Transfer(url)
将执行传输到指定的 URL,并在同一请求中处理它。 可以将事件处理程序中的 Application_Error
代码移动到自定义错误页的代码隐藏类,并将其 Global.asax
替换为以下代码:
protected void Application_Error(object sender, EventArgs e)
{
// Transfer the user to the appropriate custom error page
HttpException lastErrorWrapper =
Server.GetLastError() as HttpException;
if (lastErrorWrapper.GetHttpCode() == 404)
{
Server.Transfer("~/ErrorPages/404.aspx");
}
else
{
Server.Transfer("~/ErrorPages/Oops.aspx");
}
}
现在,当发生未经处理的异常时, Application_Error
事件处理程序会根据 HTTP 状态代码将控件传输到相应的自定义错误页。 由于控件已传输,因此自定义错误页有权访问 Server.GetLastError
未经处理的异常信息,并可以通知开发人员错误并记录其详细信息。 调用 Server.Transfer
会阻止 ASP.NET 引擎将用户重定向到自定义错误页。 相反,自定义错误页的内容作为对生成错误的页面的响应返回。
总结
在 ASP.NET Web 应用程序中发生未经处理的异常时,ASP.NET 运行时将引发 Error
事件并显示配置的错误页。 我们可以通过为 Error 事件创建事件处理程序来通知开发人员错误、记录其详细信息或以某种其他方式处理错误。 可通过两种方法为 HttpApplication
事件创建事件处理程序,例如 Error
:在 Global.asax
文件中或从 HTTP 模块创建事件处理程序。 本教程演示如何在Global.asax
文件中创建Error
事件处理程序,以通过电子邮件通知开发人员错误。
Error
如果需要以某种唯一或自定义的方式处理未经处理的异常,则创建事件处理程序非常有用。 但是,创建自己的 Error
事件处理程序来记录异常或通知开发人员并不是最有效地使用你的时间,因为已经有可用且易于使用的错误日志记录库,这些库可以在几分钟内设置。 接下来的两个教程将探讨两个此类库。
快乐编程!
深入阅读
有关本教程中讨论的主题的详细信息,请参阅以下资源: