Click here to Skip to main content
Click here to Skip to main content
Articles » Web Development » ASP.NET » General » Downloads
 
Add your own
alternative version
Go to top

User Friendly ASP.NET Exception Handling

, 20 Dec 2004
A flexible framework for user-friendly ASP.NET exception handling, and automatically notifying developers of problems before users do.
ASPNETExceptionHandling_demo.zip
WebServiceTestConsoleApplication
bin
Web References
WebReference
Reference.map
service1.disco
service1.wsdl
ASPUnhandledException
bin
wwwroot$.zip
Imports System.Reflection
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Xml
Imports System.Web
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Configuration

''' <summary>
''' Generic Unhandled error handling class for ASP.NET websites and web services. 
''' Intended as a last resort for errors which crash our application, so we can get feedback on what 
''' caused the error. Can log errors to a web page, to the event log, a local text file, or via email.
''' </summary>
''' <remarks>
''' to use:
''' 
''' 1) in ASP.NET applications:
''' 
'''     Reference the HttpModule UehHttpModule in web.config:
'''
'''         &lt;httpModules&gt;
'''	 	        &lt;add name="ASPUnhandledException" 
'''                  type="ASPUnhandledException.UehHttpModule, ASPUnhandledException" /&gt;
'''		    &lt;/httpModules&gt;
'''
''' 2) in .NET Web Services:
'''
'''     Reference the SoapExtension UehSoapExtension in web.config:
'''
'''		    &lt;soapExtensionTypes&gt;
'''				&lt;add type="ASPUnhandledException.UehSoapExtension, ASPUnhandledException"
'''				     priority="1" group="0" /&gt;
'''			&lt;/soapExtensionTypes&gt;
'''
''' more background information on Exceptions at:
'''   http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp
'''
'''  Jeff Atwood
'''  http://www.codinghorror.com/
''' </remarks>
Public Class Handler

    Private _blnLogToEventLog As Boolean = Config.GetBoolean("LogToEventLog", False)
    Private _blnLogToFile As Boolean = Config.GetBoolean("LogToFile", True)
    Private _blnLogToEmail As Boolean = Config.GetBoolean("LogToEmail", True)
    Private _blnLogToUI As Boolean = Config.GetBoolean("LogToUI", True)
    Private _strLogFilePath As String = Config.GetPath("PathLogFile")
    Private _strIgnoreRegExp As String = Config.GetString("IgnoreRegex", "")
    Private _blnIgnoreDebugErrors As Boolean = Config.GetBoolean("IgnoreDebug", True)

    Private _ResultCollection As New Specialized.NameValueCollection
    Private _strException As String = ""
    Private _strExceptionType As String = ""
    Private _strViewstate As String = ""

    Private Const _strViewstateKey As String = "__VIEWSTATE"
    Private Const _strRootException As String = "System.Web.HttpUnhandledException"
    Private Const _strRootWsException As String = "System.Web.Services.Protocols.SoapException"
    Private Const _strDefaultLogName As String = "UnhandledExceptionLog.txt"

    ''' <summary>
    ''' turns a single stack frame object into an informative string
    ''' </summary>
    Private Function StackFrameToString(ByVal sf As StackFrame) As String
        Dim sb As New StringBuilder
        Dim intParam As Integer
        Dim mi As MemberInfo = sf.GetMethod

        With sb
            '-- build method name
            .Append("   ")
            .Append(mi.DeclaringType.Namespace)
            .Append(".")
            .Append(mi.DeclaringType.Name)
            .Append(".")
            .Append(mi.Name)

            '-- build method params
            .Append("(")
            intParam = 0
            For Each param As ParameterInfo In sf.GetMethod.GetParameters()
                intParam += 1
                If intParam > 1 Then .Append(", ")
                .Append(param.Name)
                .Append(" As ")
                .Append(param.ParameterType.Name)
            Next
            .Append(")")
            .Append(Environment.NewLine)

            '-- if source code is available, append location info
            .Append("       ")
            If sf.GetFileName Is Nothing OrElse sf.GetFileName.Length = 0 Then
                .Append("(unknown file)")
                '-- native code offset is always available
                .Append(": N ")
                .Append(String.Format("{0:#00000}", sf.GetNativeOffset))

            Else
                .Append(System.IO.Path.GetFileName(sf.GetFileName))
                .Append(": line ")
                .Append(String.Format("{0:#0000}", sf.GetFileLineNumber))
                .Append(", col ")
                .Append(String.Format("{0:#00}", sf.GetFileColumnNumber))
                '-- if IL is available, append IL location info
                If sf.GetILOffset <> StackFrame.OFFSET_UNKNOWN Then
                    .Append(", IL ")
                    .Append(String.Format("{0:#0000}", sf.GetILOffset))
                End If
            End If
            .Append(Environment.NewLine)
        End With
        Return sb.ToString
    End Function

    ''' <summary>
    ''' enhanced stack trace generator
    ''' </summary>
    Private Overloads Function EnhancedStackTrace(ByVal st As StackTrace, _
        Optional ByVal strSkipClassName As String = "") As String
        Dim intFrame As Integer

        Dim sb As New StringBuilder

        sb.Append(Environment.NewLine)
        sb.Append("---- Stack Trace ----")
        sb.Append(Environment.NewLine)

        For intFrame = 0 To st.FrameCount - 1
            Dim sf As StackFrame = st.GetFrame(intFrame)
            Dim mi As MemberInfo = sf.GetMethod

            If strSkipClassName <> "" AndAlso mi.DeclaringType.Name.IndexOf(strSkipClassName) > -1 Then
                '-- don't include frames with this name
            Else
                sb.Append(StackFrameToString(sf))
            End If
        Next
        sb.Append(Environment.NewLine)

        Return sb.ToString
    End Function

    ''' <summary>
    ''' enhanced stack trace generator, using existing exception as start point
    ''' </summary>
    Private Overloads Function EnhancedStackTrace(ByVal ex As Exception) As String
        Return EnhancedStackTrace(New StackTrace(ex, True))
    End Function

    ''' <summary>
    ''' enhanced stack trace generator, using current execution as start point
    ''' </summary>
    Private Overloads Function EnhancedStackTrace() As String
        Return EnhancedStackTrace(New StackTrace(True), "ASPUnhandledException")
    End Function


    ''' <summary>
    ''' ASP.Net web-service specific exception handler, to be called from UehSoapExtension
    ''' </summary>
    ''' <returns>
    ''' string with "&lt;detail&gt;&lt;/detail&gt;" XML element, suitable for insertion into SOAP message
    ''' </returns>
    ''' <remarks>
    ''' existing SOAP detail message, prior to insertion, looks like:
    ''' 
    '''   &lt;soap:Fault&gt;
    '''     &lt;faultcode&gt;soap:Server&lt;/faultcode&gt;
    '''     &lt;faultstring&gt;Server was unable to process request.&lt;/faultstring&gt;
    '''     &lt;detail /&gt;    &lt;==  
    '''   &lt;/soap:Fault&gt;
    ''' </remarks>
    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

    ''' <summary>
    ''' ASP.Net exception handler, to be called from UehHttpModule
    ''' </summary>
    Public Sub HandleException(ByVal ex As Exception)

        '-- don't bother us with debug exceptions (eg those running on localhost)
        If _blnIgnoreDebugErrors Then
            If Diagnostics.Debugger.IsAttached Then Return
            Dim strHost As String = HttpContext.Current.Request.Url.Host.ToLower
            If strHost = "localhost" OrElse strHost = "127.0.0.1" Then
                Return
            End If
        End If

        '-- turn the exception into an informative string
        Try
            _strException = ExceptionToString(ex)
            _strExceptionType = ex.GetType.FullName
            '-- ignore root exceptions
            If _strExceptionType = _strRootException Or _strExceptionType = _strRootWsException Then
                If Not ex.InnerException Is Nothing Then
                    _strExceptionType = ex.InnerException.GetType.FullName
                End If
            End If
        Catch e As Exception
            _strException = "Error '" & e.Message & "' while generating exception string"
        End Try

        '-- some exceptions should be ignored: ones that match this regex
        '-- note that we are using the entire full-text string of the exception to test regex against
        '-- so any part of text can match.
        If Not _strIgnoreRegExp Is Nothing AndAlso _strIgnoreRegExp.Length > 0 Then
            If Regex.IsMatch(_strException, _strIgnoreRegExp, RegexOptions.IgnoreCase) Then
                Return
            End If
        End If

        '-- log this error to various locations
        Try
            '-- event logging takes < 100ms
            If _blnLogToEventLog Then ExceptionToEventLog()
            '-- textfile logging takes < 50ms
            If _blnLogToFile Then ExceptionToFile()
            '-- email takes under 1 second
            If _blnLogToEmail Then ExceptionToEmail()
        Catch e As Exception
            '-- generic catch because any exceptions inside the UEH
            '-- will cause the code to terminate immediately
        End Try

        '-- display message to the user
        If _blnLogToUI Then ExceptionToPage()
    End Sub

    ''' <summary>
    ''' turns exception into a formatted string suitable for display to a (technical) user
    ''' </summary>
    Private Function FormatExceptionForUser() As String
        Dim sb As New StringBuilder
        Dim strBullet As String = "�"

        With sb
            .Append(Environment.NewLine)
            .Append("The following information about the error was automatically captured: ")
            .Append(Environment.NewLine)
            .Append(Environment.NewLine)
            If _blnLogToEventLog Then
                .Append(" ")
                .Append(strBullet)
                .Append(" ")
                If _ResultCollection.Item("LogToEventLog") = "" Then
                    .Append("an event was written to the application log")
                Else
                    .Append("an event could NOT be written to the application log due to an error:")
                    .Append(Environment.NewLine)
                    .Append("   '")
                    .Append(_ResultCollection.Item("LogToEventLog"))
                    .Append("'")
                    .Append(Environment.NewLine)
                End If
                .Append(Environment.NewLine)
            End If
            If _blnLogToFile Then
                .Append(" ")
                .Append(strBullet)
                .Append(" ")
                If _ResultCollection.Item("LogToFile") = "" Then
                    .Append("details were written to a text log at:")
                    .Append(Environment.NewLine)
                    .Append("   ")
                    .Append(_strLogFilePath)
                    .Append(Environment.NewLine)
                Else
                    .Append("details could NOT be written to the text log due to an error:")
                    .Append(Environment.NewLine)
                    .Append("   '")
                    .Append(_ResultCollection.Item("LogToFile"))
                    .Append("'")
                    .Append(Environment.NewLine)
                End If
            End If
            If _blnLogToEmail Then
                .Append(" ")
                .Append(strBullet)
                .Append(" ")
                If _ResultCollection.Item("LogToEmail") = "" Then
                    .Append("an email was sent to: ")
                    .Append(Config.GetString("EmailTo", ""))
                    .Append(Environment.NewLine)
                Else
                    .Append("email could NOT be sent due to an error:")
                    .Append(Environment.NewLine)
                    .Append("   '")
                    .Append(_ResultCollection.Item("LogToEmail"))
                    .Append("'")
                    .Append(Environment.NewLine)
                End If
            End If
            .Append(Environment.NewLine)
            .Append(Environment.NewLine)
            .Append("Detailed error information follows:")
            .Append(Environment.NewLine)
            .Append(Environment.NewLine)
            .Append(_strException)
        End With
        Return sb.ToString
    End Function

    ''' <summary>
    ''' write an exception to the Windows NT event log
    ''' </summary>
    Private Function ExceptionToEventLog() As Boolean
        Try
            System.Diagnostics.EventLog.WriteEntry(WebCurrentUrl, _
                Environment.NewLine + _strException, _
                EventLogEntryType.Error)
            Return True
        Catch ex As Exception
            _ResultCollection.Add("LogToEventLog", ex.Message)
        End Try
        Return False
    End Function

    ''' <summary>
    ''' replace generic constants in display strings with specific values
    ''' </summary>
    Private Function FormatDisplayString(ByVal strOutput As String) As String
        Dim strTemp As String
        If strOutput Is Nothing Then
            strTemp = ""
        Else
            strTemp = strOutput
        End If
        strTemp = strTemp.Replace("(app)", Config.GetString("AppName"))
        strTemp = strTemp.Replace("(contact)", Config.GetString("ContactInfo"))
        Return strTemp
    End Function

    ''' <summary>
    ''' writes text plus newline to http response stream
    ''' </summary>
    Private Sub WriteLine(ByVal strAny As String)
        HttpContext.Current.Response.Write(strAny)
        HttpContext.Current.Response.Write(Environment.NewLine)
    End Sub

    ''' <summary>
    ''' writes current exception info to a web page
    ''' </summary>
    Private Sub ExceptionToPage()

        Dim strWhatHappened As String = Config.GetString("WhatHappened", _
            "There was an unexpected error in this website. This may be due to a programming bug.")
        Dim strHowUserAffected As String = Config.GetString("HowUserAffected", _
            "The current page will not load.")
        Dim strWhatUserCanDo As String = Config.GetString("WhatUserCanDo", _
            "Close your browser, navigate back to this website, and try repeating your last action. Try alternative methods of performing the same action. If problems persist, contact (contact).")

        Dim strPageHeader As String = Config.GetString("PageHeader", "This website encountered an unexpected problem")
        Dim strPageTitle As String = Config.GetString("PageTitle", "Website problem")

        HttpContext.Current.Response.Clear()
        HttpContext.Current.Response.ClearContent()
        WriteLine("<HTML>")
        WriteLine("<HEAD>")
        WriteLine("<TITLE>")
        WriteLine(FormatDisplayString(strPageTitle))
        WriteLine("</TITLE>")
        WriteLine("<STYLE>")
        WriteLine("body {font-family:""Verdana"";font-weight:normal;font-size: .7em;color:black; background-color:white;}")
        WriteLine("b {font-family:""Verdana"";font-weight:bold;color:black;margin-top: -5px}")
        WriteLine("H1 { font-family:""Verdana"";font-weight:normal;font-size:18pt;color:red }")
        WriteLine("H2 { font-family:""Verdana"";font-weight:normal;font-size:14pt;color:maroon }")
        WriteLine("pre {font-family:""Lucida Console"";font-size: .9em}")
        WriteLine("</STYLE>")
        WriteLine("</HEAD>")
        WriteLine("<BODY>")
        WriteLine("<H1>")
        WriteLine(FormatDisplayString(strPageHeader))
        WriteLine("<hr width=100% size=1 color=silver></H1>")
        WriteLine("<H2>What Happened:</H2>")
        WriteLine("<BLOCKQUOTE>")
        WriteLine(FormatDisplayString(strWhatHappened))
        WriteLine("</BLOCKQUOTE>")
        WriteLine("<H2>How this will affect you:</H2>")
        WriteLine("<BLOCKQUOTE>")
        WriteLine(FormatDisplayString(strHowUserAffected))
        WriteLine("</BLOCKQUOTE>")
        WriteLine("<H2>What you can do about it:</H2>")
        WriteLine("<BLOCKQUOTE>")
        WriteLine(FormatDisplayString(strWhatUserCanDo))
        WriteLine("</BLOCKQUOTE>")
        WriteLine("<INPUT type=button value=""More Info &gt;&gt;"" onclick=""this.style.display='none'; document.getElementById('MoreInfo').style.display='block'"">")
        WriteLine("<DIV style='display:none;' id='MoreInfo'>")
        WriteLine("<H2>More info:</H2>")
        WriteLine("<TABLE width=""100%"" bgcolor=""#ffffcc"">")
        WriteLine("<TR><TD>")
        WriteLine("<CODE><PRE>")
        WriteLine(FormatExceptionForUser())
        WriteLine("</PRE></CODE>")
        WriteLine("<TD><TR>")
        WriteLine("</DIV>")
        WriteLine("</BODY>")
        WriteLine("</HTML>")
        HttpContext.Current.Response.Flush()
        '-- 
        '-- If you use Server.ClearError to clear the exception in Application_Error, 
        '-- the defaultredirect setting in the Web.config file will not redirect the user 
        '-- because the unhandled exception no longer exists. If you want the 
        '-- defaultredirect setting to properly redirect users, do not clear the exception 
        '-- in Application_Error.
        '--
        HttpContext.Current.Server.ClearError()

        '-- don't let the calling page output any additional .Response HTML
        HttpContext.Current.Response.End()
    End Sub

    ''' <summary>
    ''' write current exception info to a text file; 
    ''' requires write permissions for the target folder
    ''' </summary>
    Private Function ExceptionToFile() As Boolean
        If IO.Path.GetFileName(_strLogFilePath) = "" Then
            _strLogFilePath = IO.Path.Combine(_strLogFilePath, _strDefaultLogName)
        End If
        Dim sw As IO.StreamWriter
        Try
            sw = New IO.StreamWriter(_strLogFilePath, True)
            sw.Write(_strException)
            sw.WriteLine()
            sw.Close()
            Return True
        Catch ex As Exception
            _ResultCollection.Add("LogToFile", ex.Message)
        Finally
            If Not sw Is Nothing Then
                sw.Close()
            End If
        End Try
        Return False
    End Function


    ''' <summary>
    ''' send current exception info via email
    ''' </summary>
    Private Function ExceptionToEmail() As Boolean
        Dim strMailTo As String = Config.GetString("EmailTo", "")
        If strMailTo = "" Then
            '-- don't bother mailing if we don't have anyone to mail to..
            _blnLogToEmail = False
            Return True
        End If

        Dim smtp As New SimpleMail.SmtpClient
        Dim mail As New SimpleMail.SmtpMailMessage
        With mail
            .To = strMailTo
            .From = Config.GetString("EmailFrom", "")
            .Subject = "Unhandled ASP Exception notification - " & _strExceptionType
            .Body = _strException
        End With
        If _strViewstate.Length > 0 Then
            mail.AttachmentFilename = "Viewstate.txt"
            mail.AttachmentText = _strViewstate
        End If
        Try
            smtp.SendMail(mail)
            Return True
        Catch ex As Exception
            _ResultCollection.Add("LogToEmail", ex.Message)
            '-- we're in an unhandled exception handler
        End Try
        Return False
    End Function


    ''' <summary>
    ''' exception-safe WindowsIdentity.GetCurrent retrieval; returns "domain\username"
    ''' </summary>
    ''' <remarks>
    ''' per MS, this can sometimes randomly fail with "Access Denied" on NT4
    ''' </remarks>
    Private Function CurrentWindowsIdentity() As String
        Try
            Return System.Security.Principal.WindowsIdentity.GetCurrent.Name()
        Catch ex As Exception
            Return ""
        End Try
    End Function

    ''' <summary>
    ''' exception-safe System.Environment "domain\username" retrieval
    ''' </summary>
    Private Function CurrentEnvironmentIdentity() As String
        Try
            Return System.Environment.UserDomainName + "\" + System.Environment.UserName
        Catch ex As Exception
            Return ""
        End Try
    End Function

    ''' <summary>
    ''' retrieve Process identity with fallback on error to safer method
    ''' </summary>
    Private Function ProcessIdentity() As String
        Dim strTemp As String = CurrentWindowsIdentity()
        If strTemp = "" Then
            Return CurrentEnvironmentIdentity()
        End If
        Return strTemp
    End Function

    ''' <summary>
    ''' returns current URL; "http://localhost:85/mypath/mypage.aspx?test=1&apples=bear"
    ''' </summary>
    Private Function WebCurrentUrl() As String
        Dim strUrl As String
        With HttpContext.Current.Request.ServerVariables
            strUrl = "http://" & .Item("server_name")
            If .Item("server_port") <> "80" Then
                strUrl &= ":" & .Item("server_port")
            End If
            strUrl &= .Item("url")
            If .Item("query_string").Length > 0 Then
                strUrl &= "?" & .Item("query_string")
            End If
        End With
        Return strUrl
    End Function

    ''' <summary>
    ''' returns brief summary info for all assemblies in the current AppDomain
    ''' </summary>
    Private Function AllAssemblyDetailsToString() As String
        Dim sb As New StringBuilder
        Dim nvc As Specialized.NameValueCollection
        Const strLineFormat As String = "    {0, -30} {1, -15} {2}"

        sb.Append(Environment.NewLine)
        sb.Append(String.Format(strLineFormat, _
            "Assembly", "Version", "BuildDate"))
        sb.Append(Environment.NewLine)
        sb.Append(String.Format(strLineFormat, _
            "--------", "-------", "---------"))
        sb.Append(Environment.NewLine)

        For Each a As Reflection.Assembly In AppDomain.CurrentDomain.GetAssemblies()
            nvc = AssemblyAttribs(a)
            '-- assemblies without versions are weird (dynamic?)
            If nvc("Version") <> "0.0.0.0" Then
                sb.Append(String.Format(strLineFormat, _
                    IO.Path.GetFileName(nvc("CodeBase")), _
                    nvc("Version"), _
                    nvc("BuildDate")))
                sb.Append(Environment.NewLine)
            End If
        Next

        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns more detailed information for a single assembly
    ''' </summary>
    Private Function AssemblyDetailsToString(ByVal a As Reflection.Assembly) As String
        Dim sb As New StringBuilder
        Dim nvc As Specialized.NameValueCollection = AssemblyAttribs(a)

        With sb
            .Append("Assembly Codebase:     ")
            Try
                .Append(nvc("CodeBase"))
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Assembly Full Name:    ")
            Try
                .Append(nvc("FullName"))
            Catch e As Exception
                .Append(e.message)
            End Try
            .Append(Environment.NewLine)

            .Append("Assembly Version:      ")
            Try
                .Append(nvc("Version"))
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Assembly Build Date:   ")
            Try
                .Append(nvc("BuildDate"))
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)
        End With

        Return sb.ToString
    End Function

    ''' <summary>
    ''' retrieve relevant assembly details for this exception, if possible
    ''' </summary>
    Private Function AssemblyInfoToString(ByVal ex As Exception) As String
        '-- ex.source USUALLY contains the name of the assembly that generated the exception
        '-- at least, according to the MSDN documentation..
        Dim a As System.Reflection.Assembly = GetAssemblyFromName(ex.Source)
        If a Is Nothing Then
            Return AllAssemblyDetailsToString()
        Else
            Return AssemblyDetailsToString(a)
        End If
    End Function

    ''' <summary>
    ''' gather some system information that is helpful in diagnosing exceptions
    ''' </summary>
    Private Function SysInfoToString(Optional ByVal blnIncludeStackTrace As Boolean = False) As String
        Dim sb As New StringBuilder

        With sb
            .Append("Date and Time:         ")
            .Append(DateTime.Now)
            .Append(Environment.NewLine)

            .Append("Machine Name:          ")
            Try
                .Append(Environment.MachineName)
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Process User:          ")
            .Append(ProcessIdentity)
            .Append(Environment.NewLine)

            .Append("Remote User:           ")
            .Append(HttpContext.Current.Request.ServerVariables("REMOTE_USER"))
            .Append(Environment.NewLine)

            .Append("Remote Address:        ")
            .Append(HttpContext.Current.Request.ServerVariables("REMOTE_ADDR"))
            .Append(Environment.NewLine)

            .Append("Remote Host:           ")
            .Append(HttpContext.Current.Request.ServerVariables("REMOTE_HOST"))
            .Append(Environment.NewLine)

            .Append("URL:                   ")
            .Append(WebCurrentUrl)
            .Append(Environment.NewLine)
            .Append(Environment.NewLine)

            .Append("NET Runtime version:   ")
            .Append(System.Environment.Version.ToString)
            .Append(Environment.NewLine)

            .Append("Application Domain:    ")
            Try
                .Append(System.AppDomain.CurrentDomain.FriendlyName())
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            If blnIncludeStackTrace Then
                .Append(EnhancedStackTrace())
            End If

        End With

        Return sb.ToString
    End Function

    ''' <summary>
    ''' translate an exception object to a formatted string, with additional system info
    ''' </summary>
    Private Function ExceptionToString(ByVal ex As Exception) As String
        Dim sb As New StringBuilder

        With sb
            .Append(ExceptionToStringPrivate(ex))
            '-- get ASP specific settings
            Try
                .Append(GetASPSettings())
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)
        End With

        Return sb.ToString
    End Function

    ''' <summary>
    ''' private version, called recursively for nested exceptions (inner, outer, etc)
    ''' </summary>
    Private Function ExceptionToStringPrivate(ByVal ex As Exception, _
        Optional ByVal blnIncludeSysInfo As Boolean = True) As String

        Dim sb As New StringBuilder

        If Not (ex.InnerException Is Nothing) Then
            '-- sometimes the original exception is wrapped in a more relevant outer exception
            '-- the detail exception is the "inner" exception
            '-- see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp

            '-- don't return the outer root ASP exception; it is redundant.
            If ex.GetType.ToString = _strRootException Or ex.GetType.ToString = _strRootWsException Then
                Return ExceptionToStringPrivate(ex.InnerException)
            Else
                With sb
                    .Append(ExceptionToStringPrivate(ex.InnerException, False))
                    .Append(Environment.NewLine)
                    .Append("(Outer Exception)")
                    .Append(Environment.NewLine)
                End With
            End If
        End If

        With sb
            '-- get general system and app information
            '-- we only really want to do this on the outermost exception in the stack
            If blnIncludeSysInfo Then
                .Append(SysInfoToString)
                .Append(AssemblyInfoToString(ex))
                .Append(Environment.NewLine)
            End If

            '-- get exception-specific information

            .Append("Exception Type:        ")
            Try
                .Append(ex.GetType.FullName)
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Exception Message:     ")
            Try
                .Append(ex.Message)
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Exception Source:      ")
            Try
                .Append(ex.Source)
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            .Append("Exception Target Site: ")
            Try
                .Append(ex.TargetSite.Name)
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

            Try
                .Append(EnhancedStackTrace(ex))
            Catch e As Exception
                .Append(e.Message)
            End Try
            .Append(Environment.NewLine)

        End With

        Return sb.ToString
    End Function

    ''' <summary>
    ''' exception-safe file attrib retrieval; we don't care if this fails
    ''' </summary>
    Private Function AssemblyLastWriteTime(ByVal a As System.Reflection.Assembly) As DateTime
        Try
            Return IO.File.GetLastWriteTime(a.Location)
        Catch ex As Exception
            Return DateTime.MaxValue
        End Try
    End Function

    ''' <summary>
    ''' returns build datetime of assembly, using calculated build time if possible, or filesystem time if not
    ''' </summary>
    Private Function AssemblyBuildDate(ByVal a As System.Reflection.Assembly, _
        Optional ByVal blnForceFileDate As Boolean = False) As DateTime

        Dim v As System.Version = a.GetName.Version
        Dim dt As DateTime

        If blnForceFileDate Then
            dt = AssemblyLastWriteTime(a)
        Else
            dt = CType("01/01/2000", DateTime). _
                AddDays(v.Build). _
                AddSeconds(v.Revision * 2)
            If TimeZone.IsDaylightSavingTime(dt, TimeZone.CurrentTimeZone.GetDaylightChanges(dt.Year)) Then
                dt = dt.AddHours(1)
            End If
            If dt > DateTime.Now Or v.Build < 730 Or v.Revision = 0 Then
                dt = AssemblyLastWriteTime(a)
            End If
        End If

        Return dt
    End Function

    ''' <summary>
    ''' returns string name / string value pair of all attribs for the specified assembly
    ''' </summary>
    ''' <remarks>
    ''' note that Assembly* values are pulled from AssemblyInfo file in project folder
    '''
    ''' Trademark       = AssemblyTrademark string
    ''' Debuggable      = True
    ''' GUID            = 7FDF68D5-8C6F-44C9-B391-117B5AFB5467
    ''' CLSCompliant    = True
    ''' Product         = AssemblyProduct string
    ''' Copyright       = AssemblyCopyright string
    ''' Company         = AssemblyCompany string
    ''' Description     = AssemblyDescription string
    ''' Title           = AssemblyTitle string
    ''' </remarks>
    Private Function AssemblyAttribs(ByVal a As System.Reflection.Assembly) As Specialized.NameValueCollection
        Dim Name As String
        Dim Value As String
        Dim nvc As New Specialized.NameValueCollection

        For Each attrib As Object In a.GetCustomAttributes(False)
            Name = attrib.GetType().ToString()
            Value = ""
            Select Case Name
                Case "System.Diagnostics.DebuggableAttribute"
                    Name = "Debuggable"
                    Value = CType(attrib, System.Diagnostics.DebuggableAttribute).IsJITTrackingEnabled.ToString
                Case "System.CLSCompliantAttribute"
                    Name = "CLSCompliant"
                    Value = CType(attrib, System.CLSCompliantAttribute).IsCompliant.ToString
                Case "System.Runtime.InteropServices.GuidAttribute"
                    Name = "GUID"
                    Value = CType(attrib, System.Runtime.InteropServices.GuidAttribute).Value.ToString
                Case "System.Reflection.AssemblyTrademarkAttribute"
                    Name = "Trademark"
                    Value = CType(attrib, AssemblyTrademarkAttribute).Trademark.ToString
                Case "System.Reflection.AssemblyProductAttribute"
                    Name = "Product"
                    Value = CType(attrib, AssemblyProductAttribute).Product.ToString
                Case "System.Reflection.AssemblyCopyrightAttribute"
                    Name = "Copyright"
                    Value = CType(attrib, AssemblyCopyrightAttribute).Copyright.ToString
                Case "System.Reflection.AssemblyCompanyAttribute"
                    Name = "Company"
                    Value = CType(attrib, AssemblyCompanyAttribute).Company.ToString
                Case "System.Reflection.AssemblyTitleAttribute"
                    Name = "Title"
                    Value = CType(attrib, AssemblyTitleAttribute).Title.ToString
                Case "System.Reflection.AssemblyDescriptionAttribute"
                    Name = "Description"
                    Value = CType(attrib, AssemblyDescriptionAttribute).Description.ToString
                Case Else
                    'Console.WriteLine(Name)
            End Select
            If Value <> "" Then
                If nvc.Item(Name) = "" Then
                    nvc.Add(Name, Value)
                End If
            End If
        Next

        '-- add some extra values that are not in the AssemblyInfo, but nice to have
        With nvc
            .Add("CodeBase", a.CodeBase.Replace("file:///", ""))
            .Add("BuildDate", AssemblyBuildDate(a).ToString)
            .Add("Version", a.GetName.Version.ToString)
            .Add("FullName", a.FullName)
        End With

        Return nvc
    End Function

    ''' <summary>
    ''' matches assembly by Assembly.GetName.Name; returns nothing if no match
    ''' </summary>
    Private Function GetAssemblyFromName(ByVal strAssemblyName As String) As System.Reflection.Assembly
        For Each a As [Assembly] In AppDomain.CurrentDomain.GetAssemblies()
            If a.GetName.Name = strAssemblyName Then
                Return a
            End If
        Next
    End Function

    ''' <summary>
    ''' returns formatted string of all ASP.NET collections (QueryString, Form, Cookies, ServerVariables)
    ''' </summary>
    Private Function GetASPSettings() As String
        Dim sb As New StringBuilder

        Const strSuppressKeyPattern As String = _
            "^ALL_HTTP|^ALL_RAW|VSDEBUGGER"

        With sb
            .Append("---- ASP.NET Collections ----")
            .Append(Environment.NewLine)
            .Append(Environment.NewLine)
            .Append(HttpVarsToString(HttpContext.Current.Request.QueryString, "QueryString"))
            .Append(HttpVarsToString(HttpContext.Current.Request.Form, "Form"))
            .Append(HttpVarsToString(HttpContext.Current.Request.Cookies))
            .Append(HttpVarsToString(HttpContext.Current.Session))
            .Append(HttpVarsToString(HttpContext.Current.Cache))
            .Append(HttpVarsToString(HttpContext.Current.Application))
            .Append(HttpVarsToString(HttpContext.Current.Request.ServerVariables, "ServerVariables", True, strSuppressKeyPattern))
        End With

        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns formatted string of all ASP.NET Cookies
    ''' </summary>
    Private Function HttpVarsToString(ByVal c As HttpCookieCollection) As String
        If c.Count = 0 Then Return ""

        Dim sb As New StringBuilder
        sb.Append("Cookies")
        sb.Append(Environment.NewLine)
        sb.Append(Environment.NewLine)

        For Each strKey As String In c
            AppendLine(sb, strKey, c.Item(strKey).Value)
        Next

        sb.Append(Environment.NewLine)
        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns formatted summary string of all ASP.NET app vars
    ''' </summary>
    Private Function HttpVarsToString(ByVal a As HttpApplicationState) As String
        If a.Count = 0 Then Return ""

        Dim sb As New StringBuilder
        sb.Append("Application")
        sb.Append(Environment.NewLine)
        sb.Append(Environment.NewLine)

        For Each strKey As String In a
            AppendLine(sb, strKey, a.Item(strKey))
        Next

        sb.Append(Environment.NewLine)
        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns formatted summary string of all ASP.NET Cache vars
    ''' </summary>
    Private Function HttpVarsToString(ByVal c As Caching.Cache) As String
        If c.Count = 0 Then Return ""

        Dim sb As New StringBuilder
        sb.Append("Cache")
        sb.Append(Environment.NewLine)
        sb.Append(Environment.NewLine)

        For Each de As DictionaryEntry In c
            AppendLine(sb, Convert.ToString(de.Key), de.Value)
        Next

        sb.Append(Environment.NewLine)
        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns formatted summary string of all ASP.NET Session vars
    ''' </summary>
    Private Function HttpVarsToString(ByVal s As SessionState.HttpSessionState) As String
        '-- sessions can be disabled
        If s Is Nothing Then Return ""
        If s.Count = 0 Then Return ""

        Dim sb As New StringBuilder
        sb.Append("Session")
        sb.Append(Environment.NewLine)
        sb.Append(Environment.NewLine)

        For Each strKey As String In s
            AppendLine(sb, strKey, s.Item(strKey))
        Next

        sb.Append(Environment.NewLine)
        Return sb.ToString
    End Function

    ''' <summary>
    ''' returns formatted string of an arbitrary ASP.NET NameValueCollection
    ''' </summary>
    Private Function HttpVarsToString(ByVal nvc As Specialized.NameValueCollection, ByVal strTitle As String, _
        Optional ByVal blnSuppressEmpty As Boolean = False, _
        Optional ByVal strSuppressKeyPattern As String = "") As String

        If Not nvc.HasKeys Then Return ""

        Dim sb As New StringBuilder
        sb.Append(strTitle)
        sb.Append(Environment.NewLine)
        sb.Append(Environment.NewLine)

        Dim blnDisplay As Boolean

        For Each strKey As String In nvc
            blnDisplay = True

            If blnSuppressEmpty Then
                blnDisplay = nvc(strKey) <> ""
            End If

            If strKey = _strViewstateKey Then
                _strViewstate = nvc(strKey)
                blnDisplay = False
            End If

            If blnDisplay AndAlso strSuppressKeyPattern <> "" Then
                blnDisplay = Not Regex.IsMatch(strKey, strSuppressKeyPattern)
            End If

            If blnDisplay Then
                AppendLine(sb, strKey, nvc(strKey))
            End If
        Next

        sb.Append(Environment.NewLine)
        Return sb.ToString
    End Function

    ''' <summary>
    ''' attempts to coerce the value object using the .ToString method if possible, 
    ''' then appends a formatted key/value string pair to a StringBuilder. 
    ''' will display the type name if the object cannot be coerced.
    ''' </summary>
    Private Function AppendLine(ByVal sb As StringBuilder, _
        ByVal Key As String, ByVal Value As Object) As String

        Dim strValue As String
        If Value Is Nothing Then
            strValue = "(Nothing)"
        Else
            Try
                strValue = Value.ToString
            Catch ex As Exception
                strValue = "(" & Value.GetType.ToString & ")"
            End Try
        End If

        AppendLine(sb, Key, strValue)
    End Function


    ''' <summary>
    ''' appends a formatted key/value string pair to a StringBuilder
    ''' </summary>
    Private Function AppendLine(ByVal sb As StringBuilder, _
        ByVal Key As String, ByVal strValue As String) As String

        sb.Append(String.Format("    {0, -30}{1}", Key, strValue))
        sb.Append(Environment.NewLine)
    End Function

End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

wumpus1
Web Developer
United States United States
My name is Jeff Atwood. I live in Berkeley, CA with my wife, two cats, and far more computers than I care to mention. My first computer was the Texas Instruments TI-99/4a. I've been a Microsoft Windows developer since 1992; primarily in VB. I am particularly interested in best practices and human factors in software development, as represented in my recommended developer reading list. I also have a coding and human factors related blog at www.codinghorror.com.

| Advertise | Privacy | Mobile
Web04 | 2.8.140921.1 | Last Updated 21 Dec 2004
Article Copyright 2004 by wumpus1
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid