Creating Custom Exceptions in .NET

来源:互联网 发布:pr视频剪辑软件下载 编辑:程序博客网 时间:2024/04/27 06:34

Creating Custom Exceptions in .NET

Introduction
Minimal Exceptions Types
Serialization Basics
Customizing Serialization
Closing Words

Introduction

Although the .NET framework contains all kinds of exception types which are sufficient in most cases, it can make sense to define custom exceptions in your own applications. They can greatly simplify and improve the error handling and thus increase the overall code quality. Whatever your reasons are for using custom exceptions, this article shows you how to create them and what to pay attention to when it comes to serialization, .NET guidelines and analysis tools.


System or Application?

Any custom exception you create needs to derive from the System.Exception class. You can either derive directly from it or use an intermediate exception likeSystemException or ApplicationException as base class. According to the .NET framework documentation, SystemException is meant only for those exceptions defined by the common language runtime whereas ApplicationException is intended to be used by user code:

“ApplicationException is thrown by a user program, not by the common language runtime. If you are designing an application that needs to create its own exceptions, derive from the ApplicationException class. ApplicationException extends Exception, but does not add new functionality. This exception is provided as means to differentiate between exceptions defined by applications versus exceptions defined by the system.”

So, ApplicationException seems like a good choice as base class for custom exceptions at first. But there has been quite a lot of discussion about this topic and according tothis posting by a Microsoft employee working on .NET, the usage of ApplicationException is no longer recommended. It seems that the current .NET framework and its documentation is already outdated when it comes to the ApplicationException class:

“We added ApplicationException thinking it would add value by grouping exceptions declared outside of the .NET Framework, but there is no scenario for catching ApplicationException and it only adds unnecessary depth to the hierarchy. [...] You should not define new exception classes derived from ApplicationException; use Exception instead. In addition, you should not write code that catches ApplicationException.”

So, the ApplicationException exception class will eventually be deprecated. I therefore let the custom exceptions here in this article derive directly from the Exception class.

Minimal Exceptions Types

The absolute minimum a new custom exception class needs to have is a name. Let’s say you are designing the login mechanism for a database application and as part of this job you need to create a custom exception which is thrown if a login attempt fails. A good name for such an exception would be LoginFailedException. An absolute minimum implementation in C# then looks like:

public class LoginFailedException: System.Exception{}

As you can see, all you need to do to create a basic custom exception is to derive from the Exception class. There’s only one problem with this definition. Since C# unfortunately doesn’t inherit constructors of base classes, this new type only has the standard constructor with no parameters and is therefore relatively useless. So, we need to add at least one constructor which does something useful:

public class LoginFailedException: System.Exception{   // The default constructor needs to be defined   // explicitly now since it would be gone otherwise.   public LoginFailedException()   {   }   public LoginFailedException(string message): base(message)   {   }}

By now we can use our custom exception like most other exceptions. We can throw an instance of LoginFailedException and pass a message which describes the occurred error. But .NET exceptions can do more. You can normally pass a so calledinner exception to one of the constructors which indicates that the created exception is a direct result of a previous one. This inner exception can then be retrieved via the InnerException property. This way you can build entire exceptions chains. Since this can be quite useful sometimes, we extend our existing implementation with this additional constructor:

public class LoginFailedException: System.Exception{   // ...   public LoginFailedException(string message,      Exception innerException): base(message, innerException)   {   }}

Looks good so far. We now have a working custom exception which is capable of handling error messages and inner exceptions. Great!

Serialization Basics

Let’s now have a look at what FxCop says to our exception type. In case you don’t know what FxCop is, it’s a nice analysis tool which checks .NET assemblies for conformance to the .NET framework design guidelines. FxCop doesn’t seem to like our exception yet:

  • Error for ImplementStandardExceptionConstructors:
    Add the following constructor:
    LoginFailedException(SerializationInfo, StreamingContext)
  • Error for MarkISerializableTypesWithSerializable:
    Add [Serializable] as this type implements ISerializable

Doesn’t look that great anymore, does it? However, the good news is that this can be fixed quite easily. Both errors deal with the serialization mechanism of .NET in some way. In case you don’t know what serialization is, it’s basically a way to convert an object into a form which can easily be transported or persisted. The counter part to serialization is the deserialization which is responsible for restoring the original object.

Let’s begin with the second error. Any class that is intended to be serializable needs to be marked with theSerializable attribute and since our base class Exception implements the ISerializable interface which is used to customize the serialization process, FxCop wonders why we didn’t add it. So, in order to comply with the serialization guidelines, we simply need to add the Serializable attribute and the second error is already fixed:

[Serializable]public class LoginFailedException: System.Exception{   // ...}

Correcting the first error is a bit more tedious. FxCop complains that we forgot to add an additional standard exception constructor. This constructor is used to customize the serialization process of objects. And since the base Exception class defines such a constructor and C# still lacks the support of constructor inheritance, we are better off adding such a beast:

[Serializable]public class LoginFailedException: System.Exception{   // ...   protected LoginFailedException(SerializationInfo info,      StreamingContext context): base(info, context)   {   }}

Looks good. FxCop keeps quiet now and we finally have a fully functional exception type which can be used like any predefined exception. It can handle simple error messages and inner exceptions and can even be serialized and deserialized correctly.

Customizing Serialization

Let’s pretend we want to extend our exception by being able to store no only the error message why the login attempt failed, but also which username caused the error. This is pretty straightforward. We just add a new field and a new property to our existing class:

[Serializable]public class LoginFailedException: System.Exception{   private string fUserName;   // ...   public string UserName   {      get { return this.fUserName; }      set { this.fUserName = value; }   }}

We are now able to set and get the name of the user specified during the login sequence. So far so good, but what about the serialization now? Does it still work as expected, that is, will the username be preserved when being serialized and deserialized? Unfortunately, the answer is ‘no’. You need to take care of it yourself.

Before we extend our exception to do exactly this, I tell you how the customization of the serialization and deserialization works in general. At first, you need to implement the ISerializable interface. This interface contains a single method, named GetObjectData, which is responsible for storing object fields during the serialization process. To do this, you just need to populate a passed SerializationInfo object.

The deserialization process works exactly the other way round. When an object is being deserialized, the previously filled SerializationInfo object is passed to the constructor of your class in which you are responsible for restoring the original object fields. Translated to our example exception, this looks like:

[Serializable]public class LoginFailedException: System.Exception{   private string fUserName;   // ...   protected LoginFailedException(SerializationInfo info,      StreamingContext context): base(info, context)   {      if (info != null)      {         this.fUserName = info.GetString("fUserName");      }   }   public override void GetObjectData(SerializationInfo info,      StreamingContext context)   {      base.GetObjectData(info, context);      if (info != null)      {         info.AddValue("fUserName", this.fUserName);      }   }}

Let’s begin with the serialization. Since our base Exception class already implements the ISerializable interface we can omit it in the list of implemented interfaces and base classes and only need to override and fill the GetObjectData method. At first, we call the GetObjectData method of the base class. This is needed to save the fields common to all exceptions, like the error message, stacktrace information or the inner exception. Then we simply store our username by adding it to the passed SerializationInfo object.

The deserialization works similarly. We just restore our username field in the constructor with the aid of the previously filled SerializationInfo object. To verify that our custom exception works like expected and to demonstrate how an entire serialization and deserialization process looks like, I wrote the following unit test:

using System.Runtime.Serialization.Formatters.Binary;// ...[Test]public void TestUserName(){   LoginFailedException e = new LoginFailedException();   e.UserName = "dotnet user";   using (Stream s = new MemoryStream())   {      BinaryFormatter formatter = new BinaryFormatter();      formatter.Serialize(s, e);      s.Position = 0; // Reset stream position      e = (LoginFailedException) formatter.Deserialize(s);   }   Assert.AreEqual(e.UserName, "dotnet user");}// ...

During the serialization and deserialization process, the username field is now preserved correctly and thus the Assert class remains silent and the test passes. That this is not the case if we haven’t customized the serialization process can be confirmed quite easily. Simply comment out the GetObjectData method and the call to GetString in the constructor, rerun the test and you’ll see it failing since the UserName property will now return null instead of the correct username.

Closing Words

Aside from the ApplicationException versus Exception discussion, this article has been written with the .NET 1.1 version in mind. However, I don’t expect upcoming versions of the .NET framework to differ (greatly) from the techniques described here in this article, so that the information given here should still be of value in the future. As usual, if you have any comments or questions feel free to contact me attg@gurock.com. Oh, and by the way, you can download the source of the LoginFailedException class and its testhere.