Custom Exceptions and Serialization

Posted by on Thu, Dec 22, 2022

Custom exceptions types can be very helpful… if implemented correctly!

Creating Custom Exceptions

While it is always good to use the many built in Exception types (such as ArgumentNullException), it is always a good practice to create your own custom exceptions in order to convey more meaning and information relavant to your application.

On the surface this is very simple but, if you aren’t careful, there can be hidden gotchas that can eventually trip you up as it did on my team recently. The following is an example based on what we discovered.

Basics

The basics for creating a custom exception class can easily be found in the Microsoft documentation here. Basically, your custom exception should inherit from Exception and implement 3 different constructors:

1public class InvalidProductCodeException : Exception
2{
3	public InvalidProductCodeException() : base() {}
4	public InvalidProductCodeException(string message) : base(message) {}
5	public InvalidProductCodeException(string message, Exception inner) : base(message, inner) {}
6}

Thats it! You now have a custom exception that works great if all you need is to key off of the type of exception being thrown. However, in most cases you need to include some additional information with your exception so you can make more informed decisions about what to do when it occurs.

The solution is obvious… add a property and a constructor that initializes that property!

1// code from above omitted for brevity
2
3public InvalidProductCodeException(string message, Exceptions inner, string code) : base(message, inner)
4{
5	ProductCode = code;
6}
7
8public string ProductCode { get; private set; }

Making the exception serializable

However, this is only part of the solution because exceptions need to be serializeable in order to work across domain and remoting boundaries. So, you need to mark your class as serializable and add a protected constructor that gets called by the serializer as follows:

 1[Serializable]
 2public class InvalidProductCodeException : Exception
 3{
 4    // code from above omitted for brevity
 5
 6    protected InvalidProductCodeException(SerializationInfo info, StreamingContext context)
 7    {
 8	    // Add implementation
 9    }
10}

This is what we had for quite some time that was happily working with our API service. When this exception was thrown we were able to provide the details of it back to our clients letting them know exactly what the problem was and which product code was invalid. All good… or so we thought! (I’m sure some of you have already spotted the issue)

Encountering the issue

After many years of this working, we decided to refactor some of our code into an Azure Durable Function because it was geting more complex and we wanted the procesing to be more asynchronous. So we had a Durable Orchestrator that called out to several Activities to perform the work.

What we noticed was that if the code inside of the Activity threw our InvalidProductCodeException, exception, when we tried to access the ProductCode property inside of the catch block in the Orchestrator it was always null.

After using the Durable Function Monitor plugin to VS Code to inspect the state of the Orchestrator instance, we saw that the return value from the Activity didn’t even have the ProductCode property in it.

At this point we looked closer at our custom exception and realized that while we marked the custom exception as serializable and added the protected constructor, we never provided an implementation so the custom exception properties were not getting deserialized back into the excpetion!

What happens with Durable Funtions is that the Orchestrator and Activities are essentially different processes. After an activity is done executing, it writes its state into the instance hub and exits. The Durable Function Runtime is notified that this has happened and restarts the Orchestrator that kicked off the Activity and runs up to the point that it last executed (re-entrant), picking up the serialized state/outcome from the Activity and deserializes it. Since we didn’t implement the serialization for the custom properties in our exception class, it didn’t know what to do with the ProductCode property so it ignored it.

The solution

The solution was fairly straight forward… fully implement serialization properly by providing a body to the protected constructor for serialization and then override the GetObjectData() method to be aware of the custom properties.

THe completed, corrected, custom exception class is as follows:

 1using System.Runtime.Serialization;
 2
 3[Serializable]
 4public class IvalidProductCodeException : Exception
 5{
 6	public InvalidProductCodeException() : base() {}
 7	public InvalidProductCodeException(string message) : base(message) {}
 8	public InvalidProductCodeException(string message, Exception inner) : base(message, inner) {}
 9
10	public InvalidProductCodeException(string message, Exceptions inner, string code)
11			: base(message, inner)
12	{
13		ProductCode = code;
14	}
15
16	protected InvalidProductCodeException(SerializationInfo info, StreamingContext context)
17	{
18		ProductCode = info.GetString("ProductCode");
19	}
20
21	public override void GetObjectData(SerializationInfo info, StreamingContext context)
22	{
23		if (info == null)
24		{
25			throw new ArgumentNullException(nameof(info));
26		}
27
28		info.AddValue("ProductCode", ProductCode);
29
30		// MUST call through to the base class to let it save its own state.
31		base.GetObjectData(info, context);
32	}
33
34	public string ProductCode { get; private set; }
35}

Hope this helps!