Welcome to my tutorial on Custom (Structured) Exception handling in VB.Net (C# version coming soon). In this tutorial we will look into at creating a Class Library to help with dealing with exception handling in a custom way.
As a developer all of us have the same goal, to write error free applications, but the chances of that are about the same as finding Bigfoot. As we all know, the end user will always find a way to break our applications, in ways we never thought of in the first place. We can ask them "What were doing when the error happened" and, across the board, the answer is always "I don't know." Then our next question, like we're trained robots, is "What did the error say", and we always get something from "It said something like <insert meaningless statement here>.", or "I'm not sure, I just clicked the
OK button and tried to finish, great help huh.
Most applications give basic Microsoft designed and generated messages, like the one pictured below:

This kind of message is enough to scare most end users from ever using that application again, doesn't do much for customer loyalty. So I spent hours and hours thinking that there has to be a better way of handling exceptions that inevitably happen, no one is the "perfect" programmer, except for maybe Martyr2

. All the .Net classes derive from the
SystemException, but it also provides the
ApplicationException. It is Microsoft's "
Best Practices" to inherit from the
ApplicationException class rather then the
SystemException when writing your own exception handling class.
Before asking, or stating your personal opinion, this is a discussion that has been raging for years and may never end, so I suggest you go with what you feel comfortable, I choose to inherit from
System.Exception because thats how I feel comfortable. So now lets get into our Class Library for our own structured custom exception handling, bear in mind this is a long tutorial as there just isnt any shortcut to accomplish this.
First thing you need, as with all classes you create, you ned to add and inherit the particular Namespaces you need to accomplish the tasks of your class, in this case our custom exception handling, so now the Namespaces:
CODE
Imports System.Configuration
Imports System.Runtime
Imports System.Runtime.Serialization
Imports System.Environment
Imports System.Diagnostics
Imports System.IO
Imports System
You will notice as we get into this class we have used the
<Serializable()> , meaning this class can be
Serialized, meaning it can be populated once and pass it from object to object without having to repopulate it, just deserialize it when you need to.
An exception has many, many properties, and as you know, when creating properties, whether they be ReadOnly or Read/Write, they all need their own variable, so now we're going to look at the variables we need for our Class Library, many which are instantiated once declared:
CODE
#Region "Variables Declarations"
Private _methodName As String
Private _version As String = "0.0"
Private _methodAction As String = "Unknown"
Private _culture As String = "Unknown"
Private _user As String = System.Environment.UserName
Private _hostMachine As String = System.Environment.MachineName
Private _osVersion As String = System.Environment.OSVersion.ToString
Private _exceptionDate As Date = Now.ToString("yyyy/MM/dd")
Private _exceptionTime As DateTime = Now.ToShortTimeString
Private _exceptionType As String
Private _hasInner As Boolean = False
Private _hasCulture As Boolean
Private _hasVersion As Boolean
Private _exceptionLevel As String = "Unknown"
Private _message As String = "Unknown"
Private _stackTrace As String = "Unknown"
#End Region
NOTE: You will once again notice I break my class files into different sections using the
Region Keyword, this, in my opinion, makes it easier to find thing in a large class file, such as this one. This, of course, is a design decision of all developers.
Next thing we're going to look at are the properties themselves. In this class there are 16 variables, 15 ReadOnly and 1 Read/Write. We have the ReadOnly variables because this information comes straight from the Exception being trapped so theres no need to allow the developer to set them. The single Read/Write variable allows the developer to add text describing what they were doing in the method that caused the Exception to happen in the first place. First we will look at the ReadOnly, and this is information that can be extracted directly from the Exception itself:
CODE
#Region " Exception Type "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the exception type
''' </summary>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property ExceptionType() As String
Get
Return GetBaseException.ToString.Substring(0, GetBaseException.ToString.IndexOf(":"))
End Get
End Property
#End Region
#Region " Application Culture "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the applications culture
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property ApplicationCulture() As String
Get
If _hasCulture = False Then
GetAttributes()
End If
Return _culture
End Get
End Property
#End Region
#Region " Application Version "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the Applications version
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property ApplicationVersion() As String
Get
If _hasVersion = False Then
GetAttributes()
End If
Return _version
End Get
End Property
#End Region
#Region " ApplicationName "
Public ReadOnly Property ApplicationName() As String
Get
_methodName = MyBase.Source
Return _methodName
End Get
End Property
#End Region
#Region " Error Date "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the date of the exception
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property ExceptionDate() As String
Get
Return _exceptionDate
End Get
End Property
#End Region
#Region " Error Time "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the time of the exception
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property ExceptionTime() As String
Get
Return _exceptionTime
End Get
End Property
#End Region
#Region " Machine Name "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the name of the machine the exception occurred on
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property HostMachine() As String
Get
Return _hostMachine
End Get
End Property
#End Region
#Region " Object Method "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the method that caused the exception
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property MethodName() As String
Get
Return MyBase.TargetSite.Name
End Get
End Property
#End Region
#Region "OS Version"
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the OS of the machine where the exception occurred
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property OSVersion() As String
Get
Return _osVersion
End Get
End Property
#End Region
#Region " Inner Exception "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the inner exception (if exists)
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property AppInnerException() As String
Get
If _hasInner Then
Dim pos As Integer = MyBase.InnerException.ToString.IndexOf(":")
Return MyBase.InnerException.ToString.Substring(0, pos)
Else
Return String.Empty
End If
End Get
End Property
#End Region
#Region " StackTrace "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the stacktrace of the exception
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public Overrides ReadOnly Property StackTrace() As String
Get
If Not _hasInner Then
_stackTrace = MyBase.StackTrace
Else
If Not Me.InnerException Is Nothing Then
_stackTrace = Me.InnerException.StackTrace
Else
_stackTrace = String.Empty
End If
End If
Return _stackTrace
End Get
End Property
#End Region
#Region " User Name "
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold the username of the person
''' logged into the machine where the exception occurred
''' </summary>
''' <value></value>
''' <remarks>
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public ReadOnly Property UserName() As String
Get
Return _user
End Get
End Property
#End Region
Two of the properties,
ApplicationCulture and
ApplicationVersion both call functions, we will get to these functions later, then the properties will make more sense on how they're populated. The Read/Write property is the only that allows the developer to interject his own text into the exception object, something currently not available in the current .Net
Exception Object:
CODE
#Region "Object Method Action"
''' -----------------------------------------------------------------------------
''' <summary>
''' Property to hold what the method was accomplishing when the exception occurred
''' </summary>
''' <value></value>
''' <remarks>
''' Can also be used for developer comments and trace logging
''' </remarks>
''' <history>
''' [richard.mccutchen] 8/06/05 Created
''' </history>
''' -----------------------------------------------------------------------------
Public Property MethodAction() As String
Get
Return _methodAction
End Get
Set(ByVal value As String)
If Not String.IsNullOrEmpty(value) Then
_methodAction = value
Else
_methodAction = _methodName
End If
End Set
End Property
#End Region
Next thing we're going to look at is the
Constructors of our Class Library class. Most classes need constructors in order to be initialized. In this class we have 6 constructors. 3 are recommended by Microsoft, the other 3 utilize items such as
SerializationInfo and
StreamingContext, which are used for deserializing Exception.
Instead of showing all 6 at once, Ill show the
Standard[b]constructors, then the [b]Custom constructors. First the
Standard:
CODE
#Region " Standard Constructors "
& #39;****************************************************************************
***********
' Standard constructions that are recommended by Microsoft when inheriting from
' the ApplicationException Class
& #39;****************************************************************************
***********
Public Sub New()
MyBase.New()
Me.Clear()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
Me.Clear()
End Sub
Public Sub New(ByVal message As String, ByVal innerException As Exception)
MyBase.New(message, innerException)
Me.Clear()
_hasInner = True
End Sub
#End Region
If you notice, those are pretty standard constructors for an Exception class, now we look at the
Custom constructors and they will seem to be somewhat
different than the
Standard once:
CODE
#Region " Custom Constructors "
& #39;****************************************************************************
***********
' Standard constructions utilizing items such as the methods action (what was the
' method doing when the exception occurred), and SerializationInfo & StreamingContext
' for deserializing an exception
& #39;****************************************************************************
***********
Public Sub New(ByVal methodAction As String, ByVal message As String)
MyBase.New(message)
_methodAction = methodAction
End Sub
Public Sub New(ByVal methodAction As String, ByVal message As String, ByVal innerException As Exception)
MyBase.New(message, innerException)
_methodAction = methodAction
_hasInner = True
End Sub
Public Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
MyBase.New(info, context)
_culture = info.GetString("ApplicationCulture")
_version = info.GetString("ApplicationVersion")
_hasCulture = info.GetBoolean("ApplicationCultureFlag")
_hasVersion = info.GetBoolean("ApplicationVersionFlag")
_methodAction = info.GetString("MethodAction")
_hostMachine = info.GetString("MachineName")
_user = info.GetString("UserName")
_osVersion = info.GetString("OSVersion")
_exceptionDate = info.GetDateTime("ErrorDate")
_exceptionTime = info.GetString("ErrorTime")
_exceptionLevel = info.GetString("ExceptionLevel")
_methodName = info.GetString("ApplicationName")
_exceptionType = info.GetString("ExceptionType")
_stackTrace = info.GetString("StackTrace")
End Sub
#End Region
These are the constructors that allow the developer to interject his own words on what he has doing in the method when the exception occurred. Now we can get into the meat of our Exception handler, we can discuss the methods that actually make it work. The first method we will be looking at will allow us to get the base exception of an exception. In the hierarchal listing of exceptions, there can be an exception, but 3 levels down there lies the exception that actually caused the exception in the first place. So lets take a look at that method:
CODE
#Region " GetBase Exception "
''' <summary>
''' Function to retrieve the base exception in string format
''' </summary>
''' <returns>The base exception type</returns>
''' <remarks></remarks>
Public Overrides Function GetBaseException() As Exception
Return MyBase.GetBaseException()
End Function
#End Region
Here we Override the
http://msdn2.microsoft.com/en-us/library/s...etBaseException. Overriding this in our class is what allows us to reach the Exception that was at the root of the Exception thrown. The next method we will be looking at is the [url=http://msdn2.microsoft.com/en-us/library/system.object.tostring.aspx]ToString()[/ur] Method. But in our class we will re overriding it so we can have the items we wish to have regarding the exception wee just trapped in it. Lets say that at the end of your code you have
MessageBox.Show(ex.ToString()), it would contains all the Exception information we want since we over rote the base method, it looks something like this:
CODE
#Region " To String "
''' <summary>
''' Override the base ToString function to return the exception details
''' in a our structured exception handling object
''' </summary>
''' <returns>The exception details in a comma delimited string</returns>
''' <remarks></remarks>
Public Overrides Function ToString() As String
'Return MyBase.ToString & vbCrLf & _
Return "Source Object : " & Me.Source & "," & _
"Source Method : " & MethodName & "," & _
"Method Action : " & _methodAction & "," & _
"Stack Trace : " & StackTrace & "," & _
"Inner Exception : " & AppInnerException & "," & _
"Base Excception : " & ExceptionType & "," & _
"Date : " & _exceptionDate & "," & _
"Time : " & _exceptionTime & "," & _
"Application Name : " & MyBase.Source.ToString & "," & _
"Application Version : " & _version & "," & _
"Application Culture : " & _culture & "," & _
"Username : " & _user & "," & _
"Machine Name : " & _hostMachine & "," & _
"OS Version : " & _osVersion
End Function
#End Region
Notice here we take all the items we wish to have in the
ToString() method, and have them in a comma delimited string, which we can parse at a later date if we chose to. The next method we have, which we over wrote as well, is call
GetObjectData, which we use to serialize out Exception Object. We over write this method so we can add the items we wish to have in our serialized Exception object. Here we use the
SerializationInfo Class to add all the data we have collected about out Exception to the Serialized object:
CODE
#Region " GetObjectData() "
''' <summary>
''' Procedure for getting the information about the exception so the object
''' can be serialized
''' </summary>
''' <param name="info">Data to serialize/deserialize an object (In this case an Exception)</param>
''' <param name="context">Object to contains the source of a given serialized stream</param>
''' <remarks></remarks>
Public Overrides Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext)
MyBase.GetObjectData(info, context)
'Add the items of the exception to the SerializationInfo cache for serialization
info.AddValue("ApplicationCulture", _culture)
info.AddValue("ApplicationVersion", _version)
info.AddValue("ApplicationCultureFlag", _hasCulture)
info.AddValue("ApplicationVersionFlag", _hasVersion)
info.AddValue("MethodAction", _methodAction)
info.AddValue("MachineName", _hostMachine)
info.AddValue("UserName", _user)
info.AddValue("OSVersion", _osVersion)
info.AddValue("ErrorDate", _exceptionDate)
info.AddValue("ErrorTime", _exceptionTime)
info.AddValue("ExceptionLevel", _exceptionLevel)
info.AddValue("ApplicationName", _methodName)
info.AddValue("ExceptionType", _exceptionType)
info.AddValue("StackTrace", _stackTrace)
End Sub
#End Region
The next, and ;ast 4 methods, are the main ones, they're the ones we're using to actually use
Reflection to dig as deep into the Exception object we have acquired to get all the information we can find, some of the information being the application version, and the application culture. The first method we'll look at is called
GetAttributes, and this method drills down as deep as it can into the
AssemblyQualifiedName to get as much information as possible before calling the
GetAppInfo method:
CODE
#Region " GetAttributes() "
''' <summary>
''' Procedure to retrieve the qualified name of
''' the assembly for the application that caused the exception.
''' With this information we can get the culture, version and
''' more of the application with the exception
''' </summary>
''' <remarks>
''' This method uses the ReflectedType method to retrieve the assemblies information
''' which is contained in the AssemblyQualifiedName. First it checks if theres an inner
''' exception and gets the assembly information based on the inner exception
'''</remarks>
Private Sub GetAttributes()
If _hasInner Then
If Not Me.TargetSite Is Nothing Then
If Not Me.TargetSite.ReflectedType Is Nothing Then
If Not Me.TargetSite.ReflectedType.AssemblyQualifiedName Is Nothing Then
>> GetAppInfo(Me.TargetSite.ReflectedType.AssemblyQualifiedName)
End If
End If
End If
Else
If Not Me.InnerException Is Nothing Then
If Not Me.InnerException.TargetSite Is Nothing Then
If Not Me.InnerException.TargetSite.ReflectedType Is Nothing Then
If Not Me.InnerException.TargetSite.ReflectedType.AssemblyQualifiedName Is Nothing Then
>> GetAppInfo(Me.InnerException.TargetSite.ReflectedType.AssemblyQualifiedName)
End If
End If
End If
End If
End If
_exceptionDate = Now.Date
_exceptionTime = System.DateTime.Today.ToShortTimeString
End Sub
#End Region
In all of that code you will notice the
>>, this is where it is calling this method. This methods main function is to retrieve the applications culture and version from the assembly. When I first write this Class Library last year this was one ugly method, so in time I have re factored it ti make it more readable. This method parses and splits looking for 2 main keywords
Version, and
Culture. So, lets take a look at the new re factored version:
CODE
#Region " GetAppInfo "
''' <summary>
''' Procedure for retrieving the application culture and version
''' </summary>
''' <param name="assembly">
''' The string returned from GetApplicationAttributes
''' which retrieves the assembly information for the calling application
''' </param>
''' <remarks></remarks>
Private Sub GetAppInfo(ByVal assembly As String)
'Create a string array based on the assembly string provided
Dim assemblyValue() As String = Split(assembly)
'Loop through each item in the string array
For Each item As String In assemblyValue
'Check to see if _bVersion is true or false
If _hasVersion = False Then
'Since its false we need to check the current index
'of the string array for the word "Version"
If item.IndexOf("Version", 0) >= 0 Then
'Since it contains the work "Value" we pass it t
'our GetVersion function to parse the string
'and return the version from it
_version = >> GetVersion(item)
'Set _bVersion to true
_hasVersion = True
End If
End If
'On each iteration of the loop check the
'value of our _bCulture variable for a
'false value, meaning we havent found the culture
'on the previous iterations
If _hasCulture = False Then
'Now check this index of the array for the word "Culture"
If item.IndexOf("Culture", 0) >= 0 Then
'Since it was found we need to pass this index of
'the array to our GetCulture to parse the string
'and return the culture
_culture = >> GetCulture(item)
'Set _bCulture to true
_hasCulture = True
End If
End If
'On each iteration of our loop we check to see
'if our 2 values are now true, which means we have
'the version and culture of our application
If _hasVersion And _hasCulture Then
'Exit the loop, no need to continue
Exit For
End If
Next
End Sub
#End Region
In this method, like the one before, you will notice 2 sets of
>>'s, these are the 2 functions responsible for parsing the string sent to them to look for both the version and culture of the application of the application that caused the exception. Notice I said the application, all of this is retrieving assembly information from the application that generated the exception, not the Exception item itself.
First, we'll look at the
GetVersion function, which retrieves a string delimited with equals signs (=), and based on this, if it exists, it retrieves the culture of the application where the Exception occurred:
CODE
#Region " GetVersion "
''' <summary>
''' Function to extract the applications version from the provided string
''' </summary>
''' <param name="str">String to search for the version <see cref="GetAppInfo"/></param>
''' <returns>The applications version</returns>
''' <remarks></remarks>
Private Function GetVersion(ByVal str As String) As String
'Create a string array and assign its value to the provided
'string, split at the equals sign ("=")
Dim version() As String = Split(str, "=")
'Create a temp variable to hold the first index of our string array
Dim temp As String = version(1)
'Check to see if our temp value contains a comma
If temp.IndexOf(",") >= 0 Then
'Extract the version using SubString
temp = temp.Substring(0, temp.IndexOf(","))
End If
'Return the version
Return temp
End Function
#End Region
The next function,
GetCulture is also passed a equals sign delimited string (=), if it finds the delimiter then it searches the string for the Applications version, in the same manner we search for the applications version:
CODE
#Region " GetCulture "
''' <summary>
''' Function to retrieve the applications culture from
''' the passed string
''' </summary>
''' <param name="str">String to get the culture from <see cref="GetAppInfo"/></param>
''' <returns>The applications culture</returns>
''' <remarks></remarks>
Public Function GetCulture(ByVal str As String) As String
'Create a string array and populate it the with passed
'string slit on the equals sign
Dim culture() As String = Split(str, "=")
'Create a temp variable and set its
'value to the first index of our array
Dim temp As String = culture(1)
'Check to see if the temp value contains a comma
If temp.IndexOf(",") >= 0 Then
'Since it has a comma retrieve the culture
temp = temp.Substring(0, temp.IndexOf(","))
End If
'Return the value
Return temp
End Function
#End Region
Believe it or not, thats what i takes to do custom (structured) exception handling in VB.Net, but we're not finished yet. This last method is simply used to make sure all variabes are either emptied out, or reset to their defaults each time our exception class is initialized:
CODE
#Region " Clear "
''' <summary>
''' Procedure to clear the exception items when the
''' class is initialized. This ensures we have an empty
''' item each time we cann it
''' </summary>
''' <remarks></remarks>
Private Sub Clear()
_version = "0.0"
_culture = "Unknown"
_methodAction = "Unknown"
_methodName = "Unknown"
_user = System.Environment.UserName
_hostMachine = System.Environment.MachineName
_osVersion = System.Environment.OSVersion.ToString
_hasVersion = False
_hasCulture = False
_exceptionLevel = "Unknown"
_exceptionType = "Unknown"
_stackTrace = "Unknown"
_message = "Unknown"
_exceptionDate = Now.ToString("yyyy/MM/dd")
_exceptionTime = Now.ToShortTimeString
GetAttributes()
End Sub
#End Region
That is what it takes to do your own structured custom exception handling. I wrote this Class Library at home, but it is used is several applications, both web and Windows, at work. I do suggest one thing, if you are going to use this object for applications that have many (I mean 10,000+) concurrent users do what we did, this object was altered to simply trap the exception and passw a user friendly message back to the application, it then forwarded the exception information, via a custom MessageSchema.xsd I created (making it platform independant) to a Web Service that logged the Exceptions and send either emails or SMS's to the appropriate groups based on exception level, application and time of day.
What I did leave out of here is the logging of the Exception, this was done intentionally. All developers log differently, use different databases, and some even use network flat files, so leaving that asoect out gives you the opportunity to add your own finishing touches to a very powerful object that took weeks of research and coding to complete. Since this Class Library us under the
GNU General Public License I will be distributing this class with this tutorial. With this license you can alter, modify, add or delete as you see fit, but the license file and license header must stay in tact at all times.
Thank you so much for reading, and I hope you found it useful & informative as I did while creating it. Happy Coding!
PC_ExceptionHandler.zip ( 67.14k )
Number of downloads: 206