DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 5.13 Creating a New Exception Type

Problem

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.

Solution

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 = "\t";
            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 += "\t";
             }
        }
        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);                
    }
}

Discussion

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.

[1] ThisIsCamelCasing.

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:


The default constructor

This constructor takes no arguments and simply calls the base class's default constructor.


A constructor with a parameter that accepts a message string

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.


A constructor that accepts a message string and an inner exception as parameters

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 0x80040321. 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:

  1. 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.

  2. 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:


Overriding the Equals method

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.


Overriding the GetHashCode method

Since we have overridden the Equals method, we should override the GetHashCode method, which overrides the hash code generation algorithm.


Overriding the == and != operators

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 Also

See Recipe 14.10 ; see the "Using User-Defined Exceptions" and "ApplicationException Class" topics in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section