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!