[ Team LiB ] |
Recipe 5.13 Creating a New Exception TypeProblemNone 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. SolutionCreate 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); } } DiscussionThe 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:
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.
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:
In addition, a new overridden constructor is needed that accepts information to deserialize this object.
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:
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 AlsoSee Recipe 14.10 ; see the "Using User-Defined Exceptions" and "ApplicationException Class" topics in the MSDN documentation. |
[ Team LiB ] |