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

用户友好的 ASP.NET 异常处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (66投票s)

2004 年 8 月 21 日

9分钟阅读

viewsIcon

701820

downloadIcon

4323

一个灵活的框架,用于用户友好的 ASP.NET 异常处理,并自动将问题通知开发人员,让他们比用户更早知道。

Sample Image - ASPNETExceptionHandling.gif

引言

本文是我之前在 CodeProject 发表的文章 用户友好的异常处理 的后续。该文章涵盖了控制台和 WinForms 应用程序的全局异常处理。正如我在那篇文章中提到的

这省略了一大类 .NET 应用程序:Web 应用程序。我对此进行过实验,并且不认为使用与处理控制台和 WinForms 异常相同的类来处理 Web 异常是可取的。它们有不同的需求。我在服务器端 Web 服务和 ASP.NET 应用程序中使用了这个类的变体,但它有显著不同。

本文涵盖了 ASP.NET 应用程序和 Web 服务类似的全局异常处理技术。由于这已在前一篇文章的介绍中涵盖,我将不再赘述异常的背景知识。让我们直接开始实现。

ASP.NET 网站中的未处理异常

在 ASP.NET 中设计全局异常处理程序时,您可以使用两种方法。

第一种方法最直接:使用 Global.asax 文件中的 Application_Error 事件。每当 ASP.NET 应用程序中发生未处理异常时,就会触发此事件。因此,我们可以将共享的 AspUnhandledException 类挂载在那里

Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
    Dim ueh As New ASPUnhandledException.Handler
    ueh.HandleException(Server.GetLastError.GetBaseException())
End Sub

大多数 ASP.NET 开发人员都熟悉 global.asax,因此这种方法很容易解释。它也是我在本文上一版本中使用的方法。虽然您仍然可以这样做,**但有更好的方法**。

而第二种、更好的方法是通过实现 IHttpModule 来挂载到 ASP.NET HTTP 管道

Public Class UehHttpModule
    Implements IHttpModule
    Public Sub Init(ByVal Application As System.Web.HttpApplication) 
             Implements System.Web.IHttpModule.Init
        AddHandler Application.Error, AddressOf OnError
    End Sub
    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub
    Protected Overridable Sub OnError(ByVal sender As Object, 
           ByVal args As EventArgs)
        Dim app As HttpApplication = CType(sender, HttpApplication)
        Dim ueh As New ASPUnhandledException.Handler
        ueh.Handle(app.Server.GetLastError)
    End Sub
End Class

这与第一种方法在功能上是相同的,但有一个关键区别:**您现在可以在不重新编译 ASP.NET 应用程序的情况下实现全局异常处理!** 这意味着您可以轻松地将未处理异常处理功能添加到**任何** ASP.NET 网站中 -- 只需将 ASPUnhandledException.dll 文件复制到 \bin 文件夹,然后对 Web.config 进行少量编辑即可。

<system.web>
  <httpModules>
  <add name="UehHttpModule"                 
       type="ASPUnhandledException.UehHttpModule, ASPUnhandledException" />  
  </httpModules>
</system.web>

然后您就可以开始使用了。是不是很酷?

ASP.NET Web 服务中的未处理异常

不幸的是,这两种方法都无法用于 .NET Web 服务。Application_Error 永远不会触发,并且 HTTP 管道被绕过,取而代之的是 SOAP 管道。要为 Web 服务全局处理异常,我们必须实现一个 SoapExtension

Public Class UehSoapExtension
    Inherits SoapExtension
    
    Private _OldStream As Stream
    Private _NewStream As Stream
    
    Public Overloads Overrides Function GetInitializer( _
    ByVal serviceType As System.Type) As Object
        Return Nothing
    End Function
    
    Public Overloads Overrides Function GetInitializer( _
    ByVal methodInfo As System.Web.Services.Protocols.LogicalMethodInfo,  _
    ByVal attribute As System.Web.Services.Protocols.SoapExtensionAttribute) 
    As Object
        Return Nothing
    End Function
    
    Public Overrides Sub Initialize(ByVal initializer As Object)
    End Sub
    
    Public Overrides Function ChainStream(ByVal stream As Stream) As Stream
        _OldStream = stream
        _NewStream =  New MemoryStream 
        Return _NewStream 
    End Function
    
    Private Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)
        Dim sr As New StreamReader(fromStream)
        Dim sw As New StreamWriter(toStream)
        sw.Write(sr.ReadToEnd())
        sw.Flush()
    End Sub
    
    Public Overrides Sub ProcessMessage(ByVal message _
    As System.Web.Services.Protocols.SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeDeserialize
                Copy(_OldStream, _NewStream)
                _NewStream.Position = 0
            Case SoapMessageStage.AfterSerialize
                If Not message.Exception Is Nothing Then
                    Dim ueh As New Handler
                    Dim strDetailNode As String
                    '-- handle our exception, and get the SOAP <detail> string

                    strDetailNode = ueh.HandleWebServiceException(message)
                    '-- read the entire SOAP message stream into a string

                    _NewStream.Position = 0                    
                    Dim tr As TextReader = New StreamReader(_NewStream)
                    '-- insert our exception details into the string

                    Dim s As String = tr.ReadToEnd
                    s = s.Replace("<detail />", strDetailNode)
                    '-- overwrite the stream with our modified string

                    _NewStream = New MemoryStream
                    Dim tw As TextWriter = New StreamWriter(_NewStream)
                    tw.Write(s)
                    tw.Flush()
                End If
                _NewStream.Position = 0
                Copy(_NewStream, _OldStream)
        End Select
    End Sub
    
End Class

SoapExtension 比我们的 HttpModule 复杂一些,因为我们必须修改 SOAP 消息以包含详细的异常信息。我稍后会详细介绍这一点。要让它在您的 Web 服务中正常工作,您只需将 ASPUnhandledException.dll 文件复制到 \bin 文件夹,然后对 Web.config 进行少量编辑即可。

<webServices>
  <soapExtensionTypes>
    <add type="ASPUnhandledException.UehSoapExtension, ASPUnhandledException"
         priority="1" group="0" />
  </soapExtensionTypes>
</webServices>

配置 AspUnhandledException

AspUnhandledException 类通过 Web.config 中的一个自定义配置节 <UnhandledException> 进行配置。该类在没有配置的情况下也能工作,但默认情况下您只会获得网站根目录下的一个纯文本日志文件。我建议将以下内容作为最低限度的实践添加到您的 Web.config 中。

<configSections>    
  <section name="UnhandledException" 
     type="System.Configuration.NameValueSectionHandler, System, 
           Version=1.0.5000.0, Culture=neutral, 
           PublicKeyToken=b77a5c561934e089" />
</configSections>

<UnhandledException>
  <add key="ContactInfo" value="Ima Testguy at 123-555-1212" />
  <add key="EmailTo" value="me@mydomain.com" />
  <add key="SmtpDefaultDomain" value="mydomain.com" />
  <add key="SmtpServer" value="mail.mydomain.com" />
</UnhandledException>

这是配置设置的完整列表

默认值 描述
EmailTo none 用于发送异常通知的电子邮件地址的分号分隔列表
EmailFrom server@domain.com 覆盖异常电子邮件中提供的发件人地址。可选。
IgnoreDebug True 当调试器附加时,或者当原始异常主机是 localhost 时,不处理任何异常。如果您想调试错误处理程序,请确保将其设置为 False
IgnoreRegex "" 如果此正则表达式模式对异常字符串的任何部分求值为 true,则忽略此异常。
LogToEventLog 将未处理的异常事件写入事件日志
LogToEmail True 将未处理的异常电子邮件发送到 EmailTo 中的地址
LogToFile True 将未处理的异常条目写入纯文本日志文件
LogToUI True 自动显示一个自定义的未处理异常 HTML 页面
PathLogFile "" 纯文本异常日志文件的路径。可以是完全限定的或相对的,带或不带文件名。如果是相对路径,则假定相对于网站的根文件夹。如果未提供路径,则在网站根目录下使用默认文件名。
AppName none 在默认错误 HTML 页面上用于替换字符串中出现的 "(app)" 的实例
ContactInfo none 在默认错误 HTML 页面上用于替换字符串中出现的 "(contact)" 的实例
PageTitle (参见截图) 默认错误 HTML 页面的标题
PageHeader (参见截图) 默认错误 HTML 页面的标题
WhatHappened (参见截图) 在默认错误 HTML 网页的 "What happened:" 标题下显示的文本
HowUserAffected (参见截图) 在默认错误 HTML 网页的 "How this will affect you:" 标题下显示的文本
WhatUserCanDo (参见截图) 在默认错误 HTML 网页的 "What you can do about it:" 标题下显示的文本

.dll 文件复制到 /bin 文件夹并检查完 Web.config 设置后,**其他一切都将自动完成**。

ASPUnhandledException 与 ASP.NET 网站

好的,现在您已经设置好了,您的努力得到了什么回报?嗯,当您的网站或 Web 服务中发生未处理异常时,以下一项或多项将自动发生:

  1. 向用户显示一个用户友好的网页
  2. 将电子邮件通知发送到您选择的地址
  3. 将纯文本日志文件写入您选择的路径和文件名
  4. 将条目写入事件日志

所有这些选项都将包含由 ExceptionToString 生成的关于异常的详细诊断信息。我现在将详细介绍每个事件。

如果 LogToUI 保持默认值 True,我们将根据 Alan Cooper 在其著作 《About Face: The Essentials of User Interface Design》 中 "The End of Errors" 章节中概述的 GUI 设计自动生成一个用户友好的网页。请注意,如果您关闭 LogToUI,您将恢复到默认的 ASP.NET "黄色死亡屏幕" 页面。

screenshot of custom unhandled exception web page

如果 LogToEmail 保持默认值 True,您的未处理异常将通过捆绑的托管 SMTP 类 SimpleMail 自动发送电子邮件。该类需要 Web.config 自定义配置节 <UnhandledException> 中的有效 SMTP 设置。配置设置的完整列表是:

默认值 描述
SmtpDefaultDomain none 未提供域名的电子邮件的默认电子邮件域名
SmtpServer none 用于发送电子邮件的 SMTP 电子邮件服务器
SmtpPort 25 用于发送电子邮件的 SMTP 端口
SmtpAuthUser none 如果启用了出站邮件身份验证,则用于身份验证的用户名
SmtpAuthPassword none 如果启用了出站邮件身份验证,则用于身份验证的密码

异常电子邮件包含

  • 用户和计算机标识
  • 应用程序信息
  • 异常摘要
  • 自定义堆栈跟踪
  • 所有 ASP.NET 集合内容

screenshot of automatic exception email

如果 LogToFile 选项保持默认值 True,未处理的异常将写入您网站根目录下的纯文本日志文件,名为 UnhandledExceptionLog.txt。如果您需要不同的路径,请使用 PathLogFile 设置来指定相对路径或绝对路径,带或不带文件名。如果路径是相对的,则假定相对于当前网站的根目录。您指定的任何路径都必须具有 ASP.NET 的写入权限,否则日志记录将静默失败。

如果启用了 LogToEventLog,您将在应用程序事件日志中看到相同的详细异常信息。请注意,您必须授予 ASP.NET 进程帐户写入注册表的权限才能使其工作。我通常不在我的应用程序中这样做,这就是为什么我将其默认设置为 False,但它确实有效!

ASPUnhandledException 与 ASP.NET Web 服务

在我之前的文章 中,我曾哀叹 ASP.NET Web 服务缺乏良好的全局错误处理机制。现在不再是这样了。使用自定义 SoapExtension,这很容易!而且,它通常与 ASP.NET 中的工作方式相同。但是,您应该注意一些重要的区别:

  1. 使用 SOAPExtension 意味着我们**只**捕获 SOAP 未处理异常-- **您将无法使用 Web 浏览器界面测试未处理的异常!** 请牢记这一点!因此,我在解决方案中包含了一个演示 SOAP 控制台应用程序。
  2. SOAP 客户端没有浏览器界面,因此 LogToUI 选项不适用于 Web 服务,在此情况下将被忽略。
  3. SoapException 与典型的 .NET Exception 有点不同,它包含一个特殊的 XML <detail> 元素,用于详细说明服务器端的异常。

剩下的唯一棘手的部分是将我们期望从 ASP.NET 异常中获得的丰富异常信息插入到 SOAP 消息中。默认情况下,<detail> 元素非常稀疏,因此从服务器到客户端几乎没有信息传递。

<soap:Fault>
  <faultcode>soap:Server</faultcode>
  <faultstring>SoapException</faultstring>
  <detail/>
</soap:Fault>

很难诊断出信息如此之少的异常。这就是 Handler.HandleWebServiceException 的用武之地。我们从 SoapExtension 调用此方法,它会为我们生成一个更好的 <detail> 元素。

Public Function HandleWebServiceException(ByVal sm As _
              System.Web.Services.Protocols.SoapMessage) As String
    _blnLogToUI = False
    HandleException(sm.Exception)
    
    Dim doc As New Xml.XmlDocument
    Dim DetailNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        SoapException.DetailElementName.Name, _
        SoapException.DetailElementName.Namespace)
    
    Dim TypeNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionType", _
        SoapException.DetailElementName.Namespace)
    TypeNode.InnerText = _strExceptionType
    DetailNode.AppendChild(TypeNode)
    
    Dim MessageNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionMessage", _
        SoapException.DetailElementName.Namespace)
    MessageNode.InnerText = sm.Exception.Message
    DetailNode.AppendChild(MessageNode)
    
    Dim InfoNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
        "ExceptionInfo", _
        SoapException.DetailElementName.Namespace)
    InfoNode.InnerText = _strException
    DetailNode.AppendChild(InfoNode)
    
    Return DetailNode.OuterXml.ToString()
End Function

此方法返回的字符串用于在"飞行中"修改 SOAP 消息,并且我们获得了一个更好的 <detail> 节点。

<soap:Fault>
  <faultcode>soap:Server</faultcode>
  <faultstring>Server was unable to process request. -->

我修剪了许多 <ExceptionInfo> 行,但它与您之前看到的丰富诊断信息相同。

结论

在我们结束讨论之前,关于未处理异常还有最后一点观察。**您必须避免在未处理异常处理程序中发生异常**。这是一个特殊的处理程序,是"最后一道防线"的处理程序,在此代码中发生的异常是极其不当的。它们可能导致代码毫无警告地终止,就像您在函数中间任意放置一个 Return 一样。我在处理程序中采取了极大的预防措施来避免异常,但请当心。

到目前为止,我已将此类用于大约十二个不同的网站和 Web 服务,并取得了优异的成果。文章顶部的源代码提供了更多细节和注释,请查看。请不要犹豫,提供好坏参半的反馈!

希望您喜欢这篇文章。如果喜欢,您可能还会喜欢 我的其他文章

历史

  • 2004 年 8 月 21 日星期六 - 发布
  • 2004 年 9 月 27 日星期一 - 更新了演示代码
    • 修复了在 global.asax 中内部异常被丢弃的问题。
    • 添加了忽略最外层 ASP.NET 异常的代码(因为它每次都相同)。
    • 删除了在 AssemblyInfo 中未填充 Company 和 Product 时以前会抛出的 ConfigurationException
  • 2004 年 10 月 17 日星期日 - 2.0 版
    • 添加了 SoapExtensionHttpHandler
    • 改进了基类 Handler(并修复了一些错误)。
    • 重新构建了文章和演示解决方案。
  • 2004 年 12 月 18 日星期六 - 2.1 版
    • 使用了单独的自定义 <UnhandledException> .config 文件节。
    • 添加了 .configEmailFromPageTitlePageHeader
    • 视图状态作为纯文本电子邮件附件发送(如果存在)。
    • 对 ASP.NET 集合进行了更智能、更简洁的格式化。
    • 添加了 ASP.NET Session、Cache 和 Application 集合摘要。
    • 当通过电子邮件、文件或事件日志记录失败时,在 LogToUi 页面中提供了更多反馈。
    • HTML 错误页面的 "More Details:" 部分现在是一个展开按钮,以便为普通用户简化输出。
    • 转换为 VB.NET 2005 风格的 XML 注释。
© . All rights reserved.