None of the built-in exceptions in the .NET Framework provide the implementation details that you require for an exception that you need to throw. You need to create your own exception class that operates seamlessly with your application, as well as other applications. Whenever an application receives this new exception, it can inform the user that a specific error occurred in a specific component. This report will greatly reduce the time required to debug the problem.
Create your own exception class. To illustrate,
we’ll create a custom exception class,
RemoteComponentException
, that will inform a
client application that an error has occurred in a remote server
assembly. The complete source code for the
RemoteComponentException
class
is:
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; [SerializableAttribute] public class RemoteComponentException : ApplicationException, ISerializable { // New exception field private string serverName = ""; // Normal exception ctor's public RemoteComponentException( ) : base( ) { } public RemoteComponentException(string message) : base(message) { } public RemoteComponentException(string message, Exception innerException) : base(message, innerException) { } // Exception ctor's that accept the new ServerName parameter public RemoteComponentException(string message, string serverName) : base(message) { this.serverName = serverName; } public RemoteComponentException(string message, Exception innerException, string serverName) : base(message, innerException) { this.serverName = serverName; } // Serialization ctor public RemoteComponentException(SerializationInfo exceptionInfo, StreamingContext exceptionContext) : base(exceptionInfo, exceptionContext) { this.serverName = exceptionInfo.GetString("ServerName"); } // Read-only property public string ServerName { get{return (serverName.Trim( ));} } public override string Message { get { if (this.ServerName.Length == 0) return (base.Message + Environment.NewLine + "An unnamed server has encountered an error."); else return (base.Message + Environment.NewLine + "The server " + this.ServerName + " has encountered an error."); } } // Overridden methods // ToString method public override string ToString( ) { string errorString = "An error has occured in a server " + "component of this client."; errorString += Environment.NewLine + "Server Name: " + this.ServerName; if (this.InnerException == null) { errorString += Environment.NewLine + "Server component failed to provide an " + "underlying exception!"; } else { string indent = " "; Exception ie = this; while(ie.InnerException != null) { ie = ie.InnerException; errorString += Environment.NewLine + indent + "inner exception type thrown by server component: " + ie.GetType( ).Name.ToString( ); errorString += Environment.NewLine + indent + "Message: " + ie.Message; errorString += Environment.NewLine + indent + "StackTrace: " + ie.StackTrace; indent += " "; } } errorString += Environment.NewLine + "StackTrace of client " + "component: " + this.StackTrace; return (errorString); } // Call base.ToString method public string ToBaseString( ) { return (base.ToString( )); } // GetHashCode public override int GetHashCode( ) { return (ServerName.GetHashCode( )); } // Equals public override bool Equals(object obj) { bool isEqual = false; if (obj == null || (this.GetType( ) != obj.GetType( ))) { isEqual = false; } else { RemoteComponentException se = (RemoteComponentException)obj; if ((this.ServerName.Length == 0) && (se.ServerName.Length == 0)) isEqual = false; else isEqual = (this.ServerName == se.ServerName); } return (isEqual); } // == operator public static bool operator ==(RemoteComponentException v1, RemoteComponentException v2) { return (v1.Equals(v2)); } // != operator public static bool operator !=(RemoteComponentException v1, RemoteComponentException v2) { return (!(v1 == v2)); } // Used during serialization to capture information about extra fields public override void GetObjectData(SerializationInfo exceptionInfo, StreamingContext exceptionContext) { base.GetObjectData(exceptionInfo, exceptionContext); exceptionInfo.AddValue("ServerName", this.ServerName); } }
The code to test the RemoteComponentException
class is:
public void TestSpecializedException( ) { // Generic inner exception used to test the // RemoteComponentException's inner exception Exception inner = new Exception("The inner Exception"); // Test each ctor Console.WriteLine(Environment.NewLine + Environment.NewLine + "TEST EACH CTOR"); RemoteComponentException se1 = new RemoteComponentException ( ); RemoteComponentException se2 = new RemoteComponentException ("A Test Message for se2"); RemoteComponentException se3 = new RemoteComponentException ("A Test Message for se3", inner); RemoteComponentException se4 = new RemoteComponentException ("A Test Message for se4", "MyServer"); RemoteComponentException se5 = new RemoteComponentException ("A Test Message for se5", inner, "MyServer"); // Test new ServerName property Console.WriteLine(Environment.NewLine + "TEST NEW SERVERNAME PROPERTY"); Console.WriteLine("se1.ServerName == " + se1.ServerName); Console.WriteLine("se2.ServerName == " + se2.ServerName); Console.WriteLine("se3.ServerName == " + se3.ServerName); Console.WriteLine("se4.ServerName == " + se4.ServerName); Console.WriteLine("se5.ServerName == " + se5.ServerName); // Test overridden Message property Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- MESSAGE PROPERTY"); Console.WriteLine("se1.Message == " + se1.Message); Console.WriteLine("se2.Message == " + se2.Message); Console.WriteLine("se3.Message == " + se3.Message); Console.WriteLine("se4.Message == " + se4.Message); Console.WriteLine("se5.Message == " + se5.Message); // Test -overridden- ToString method Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- TOSTRING METHOD"); Console.WriteLine("se1.ToString( ) == " + se1.ToString( )); Console.WriteLine("se2.ToString( ) == " + se2.ToString( )); Console.WriteLine("se3.ToString( ) == " + se3.ToString( )); Console.WriteLine("se4.ToString( ) == " + se4.ToString( )); Console.WriteLine("se5.ToString( ) == " + se5.ToString( )); // Test ToBaseString method Console.WriteLine(Environment.NewLine + "TEST TOBASESTRING METHOD"); Console.WriteLine("se1.ToBaseString( ) == " + se1.ToBaseString( )); Console.WriteLine("se2.ToBaseString( ) == " + se2.ToBaseString( )); Console.WriteLine("se3.ToBaseString( ) == " + se3.ToBaseString( )); Console.WriteLine("se4.ToBaseString( ) == " + se4.ToBaseString( )); Console.WriteLine("se5.ToBaseString( ) == " + se5.ToBaseString( )); // Test -overridden- Equals method Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- EQUALS METHOD"); Console.WriteLine("se1.Equals(se1) == " + se1.Equals(se1)); Console.WriteLine("se2.Equals(se1) == " + se2.Equals(se1)); Console.WriteLine("se3.Equals(se1) == " + se3.Equals(se1)); Console.WriteLine("se4.Equals(se1) == " + se4.Equals(se1)); Console.WriteLine("se5.Equals(se1) == " + se5.Equals(se1)); Console.WriteLine("se5.Equals(se4) == " + se5.Equals(se4)); // Test -overridden- == operator Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- == OPERATOR"); Console.WriteLine("se1 == se1 == " + (se1 == se1)); Console.WriteLine("se2 == se1 == " + (se2 == se1)); Console.WriteLine("se3 == se1 == " + (se3 == se1)); Console.WriteLine("se4 == se1 == " + (se4 == se1)); Console.WriteLine("se5 == se1 == " + (se5 == se1)); Console.WriteLine("se5 == se4 == " + (se5 == se4)); // Test -overridden- != operator Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- != OPERATOR"); Console.WriteLine("se1 != se1 == " + (se1 != se1)); Console.WriteLine("se2 != se1 == " + (se2 != se1)); Console.WriteLine("se3 != se1 == " + (se3 != se1)); Console.WriteLine("se4 != se1 == " + (se4 != se1)); Console.WriteLine("se5 != se1 == " + (se5 != se1)); Console.WriteLine("se5 != se4 == " + (se5 != se4)); // Test -overridden- GetBaseException method Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- GETBASEEXCEPTION METHOD"); Console.WriteLine("se1.GetBaseException( ) == " + se1.GetBaseException( )); Console.WriteLine("se2.GetBaseException( ) == " + se2.GetBaseException( )); Console.WriteLine("se3.GetBaseException( ) == " + se3.GetBaseException( )); Console.WriteLine("se4.GetBaseException( ) == " + se4.GetBaseException( )); Console.WriteLine("se5.GetBaseException( ) == " + se5.GetBaseException( )); // Test -overridden- GetHashCode method Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- GETHASHCODE METHOD"); Console.WriteLine("se1.GetHashCode( ) == " + se1.GetHashCode( )); Console.WriteLine("se2.GetHashCode( ) == " + se2.GetHashCode( )); Console.WriteLine("se3.GetHashCode( ) == " + se3.GetHashCode( )); Console.WriteLine("se4.GetHashCode( ) == " + se4.GetHashCode( )); Console.WriteLine("se5.GetHashCode( ) == " + se5.GetHashCode( )); // Test serialization Console.WriteLine(Environment.NewLine + "TEST SERIALIZATION/DESERIALIZATION"); BinaryFormatter binaryWrite = new BinaryFormatter( ); Stream ObjectFile = File.Create("se1.object"); binaryWrite.Serialize(ObjectFile, se1); ObjectFile.Close( ); ObjectFile = File.Create("se2.object"); binaryWrite.Serialize(ObjectFile, se2); ObjectFile.Close( ); ObjectFile = File.Create("se3.object"); binaryWrite.Serialize(ObjectFile, se3); ObjectFile.Close( ); ObjectFile = File.Create("se4.object"); binaryWrite.Serialize(ObjectFile, se4); ObjectFile.Close( ); ObjectFile = File.Create("se5.object"); binaryWrite.Serialize(ObjectFile, se5); ObjectFile.Close( ); BinaryFormatter binaryRead = new BinaryFormatter( ); ObjectFile = File.OpenRead("se1.object"); object Data = binaryRead.Deserialize(ObjectFile); Console.WriteLine("----------" + Environment.NewLine + Data); ObjectFile.Close( ); ObjectFile = File.OpenRead("se2.object"); Data = binaryRead.Deserialize(ObjectFile); Console.WriteLine("----------" + Environment.NewLine + Data); ObjectFile.Close( ); ObjectFile = File.OpenRead("se3.object"); Data = binaryRead.Deserialize(ObjectFile); Console.WriteLine("----------" + Environment.NewLine + Data); ObjectFile.Close( ); ObjectFile = File.OpenRead("se4.object"); Data = binaryRead.Deserialize(ObjectFile); Console.WriteLine("----------" + Environment.NewLine + Data); ObjectFile.Close( ); ObjectFile = File.OpenRead("se5.object"); Data = binaryRead.Deserialize(ObjectFile); Console.WriteLine("----------" + Environment.NewLine + Data + Environment.NewLine + "----------"); ObjectFile.Close( ); Console.WriteLine(Environment.NewLine + "END TEST" + Environment.NewLine); }
The exception hierarchy starts with the Exception
class; from this, two classes are derived:
ApplicationException
and
SystemException
. The
SystemException
class and any classes derived from
it are reserved for the developers of the FCL. Most of the common
exceptions, such as the NullReferenceException
or
the OverflowException
exceptions, are derived from
SystemException
. The FCL developers created the
ApplicationException
class for other developers
using the .NET languages to derive their own exceptions from. This
partitioning allows for a clear distinction between user-defined
exceptions and the built-in system exceptions. Nothing actively
prevents you from deriving a class from the
SystemException
class, but it is better to be
consistent and use the convention of always deriving from the
ApplicationException
class for user-defined
exceptions.
You should follow the naming convention for exceptions when
determining the name of your exception. The convention is very
simple. Whatever you decide on for the exception’s
name, add the word Exception
to the end of the
name (e.g., use UnknownException
as the exception
name instead of just Unknown
). In addition, the
name should be camel-cased[1] and contain no underscore characters.
Every user-defined exception should include at
least
three constructors, described next. This is not a
requirement, but it makes your exception classes operate similar to
every other exception class in the FCL and minimizes the learning
curve for other developers using your new exception. These three
constructors are:
This constructor takes no arguments and simply calls the base class’s default constructor.
This message string overwrites the default contents of the
Message
field of this exception. Like the default
constructor, this constructor also calls the base
class’s constructor, which also accepts a message
string as its only parameter.
The object contained in the innerException
parameter is added to the InnerException
field of
this exception object. Like the other two constructors, this
constructor calls the base class’ constructor of the
same signature.
If this exception will be caught in unmanaged code, such as a COM
object, you can also override the
HRESULT
value
for this exception. An exception caught in unmanaged code becomes an
HRESULT
value. If the exception does not override
the HRESULT
value, it defaults to the
HRESULT
value of the base class exception, which,
in the case of a user-defined exception object that inherits from
ApplicationException
, is
HRESULT
COR_E_APPLICATION
,
which has a value of 0x80131600
. To override the
default HRESULT
value, simply change the value of
this field in the constructor. The following code demonstrates this
technique:
public class RemoteComponentException : ApplicationException { public RemoteComponentException( ) : base( ) { HResult = 0x80040321; } public RemoteComponentException(string message) : base(message) { HResult = 0x80040321; } public RemoteComponentException(string message, Exception innerException) : base(message, innerException) { HResult = 0x80040321; } }
Now the HResult
that the COM object will see is
the value 0x
80040321. See Table 5-2 in Recipe 5.8 for
more information on the mapping of HRESULT
values
to their equivalent managed exception classes.
It is usually a good idea to override the Message
field in order to incorporate any new fields into the
exception’s message text. Always remember to include
the base class’s message text along with any
additional text you add to this property.
Fields and their accessors should be created to hold data specific to
the exception. Since this exception will be thrown as a result of an
error that occurs in a remote server assembly, we will add a private
field to contain the name of the server or service. In addition, we
will add a public read-only property to access this field. Since we
added this new field, we should add two constructors that accept an
extra parameter used to set the value of the
serverName
field.
If necessary, override any base class members whose behavior is
inherited by the custom exception class. For example, since we have
added a new field, we need to determine whether it will need to be
added to the default contents of the Message
field
for this exception. If it does, we must override the
Message
property.
Notice that the Message
field in the base class is
displayed on the first line and our additional text is displayed on
the next line. This organization takes into account that a user might
modify the message that will appear in the Message
field by using one of the overloaded constructors that takes a
message string as a parameter.
In certain cases (such as remoting), your exception object should be serializable and deserializable. This involves performing the following two additional steps:
Add the Serializable
attribute to the class
definition. This attribute specifies that this class can be
serialized or deserialized. A
SerializationException
is thrown if this attribute
does not exist on this class and an attempt is made to serialize this
class.
The class should implement the ISerializable
interface if you want control over how serialization and
deserialization are performed, and it should provide an
implementation for its single member,
GetObjectData
. Here we implement it because the
base class implements it, which means that we have no choice but to
reimplement it if we want the fields we added (e.g.,
serverName
) to get serialized.
In addition, a new overridden constructor is needed that accepts information to deserialize this object.
Even though it is not required, you should make all user-defined exception classes serializable and deserializable.
At this point, the RemoteComponentException
class
contains everything you need for a complete user-defined exception
class. You could stop at this point, but let’s
continue a bit farther and override some default functionality that
deals with the hash code, equality, and inequality:
It is
possible that we might need to override the default implementation of
the Equals
method and the ==
and !=
operators. The default implementation tests
each object for reference equality. We may need to test for value
equality; in this case, we need to override this method and both
operators. The ServerName
property value will be
used in determining equality between two
RemoteComponentException
classes. The
Equals
method returns true
only
if the ServerName
properties of both
RemoteComponentException
objects return the same
value. Otherwise, the two objects are not considered equal. The
exception occurs when the ServerName
properties of
both objects are blank. In this case, both
RemoteComponentException
objects are considered to
be in an unknown state and therefore equality cannot be definitely
determined.
Since we
have overridden the Equals
method, we should
override the GetHashCode
method, which overrides
the hash code generation algorithm.
When
overriding the Equals
method, both the
== and != operators should be
overloaded as well. Notice that both operators ultimately use the
Equals
method to determine equality. Therefore,
they are simple to write.
As a final note, it is wise to place all user-defined exceptions in a separate assembly, which allows for easier reuse of these exceptions in other applications, and, more importantly, allows other application domains and remotely executing code to both throw and handle these exceptions correctly no matter where they are thrown. The assembly that holds these exceptions should be signed with a strong name and added to the Global Assembly Cache (GAC) so that any code that uses or handles these exceptions can find the assembly that defines them. See Recipe 14.10 for more information on how to do this.
See Recipe 14.10 ; see the “Using User-Defined Exceptions” and “ApplicationException Class” topics in the MSDN documentation.