248

More specifically, when the exception contains custom objects which may or may not themselves be serializable.

Take this example:

public class MyException : Exception
{
    private readonly string resourceName;
    private readonly IList<string> validationErrors;

    public MyException(string resourceName, IList<string> validationErrors)
    {
        this.resourceName = resourceName;
        this.validationErrors = validationErrors;
    }

    public string ResourceName
    {
        get { return this.resourceName; }
    }

    public IList<string> ValidationErrors
    {
        get { return this.validationErrors; }
    }
}

If this Exception is serialized and de-serialized, the two custom properties (ResourceName and ValidationErrors) will not be preserved. The properties will return null.

Is there a common code pattern for implementing serialization for custom exception?

Daniel Fortunov
  • 38,854
  • 23
  • 76
  • 101

8 Answers8

459

Base implementation, without custom properties

SerializableExceptionWithoutCustomProperties.cs:

namespace SerializableExceptions
{
    using System;
    using System.Runtime.Serialization;

    [Serializable]
    // Important: This attribute is NOT inherited from Exception, and MUST be specified 
    // otherwise serialization will fail with a SerializationException stating that
    // "Type X in Assembly Y is not marked as serializable."
    public class SerializableExceptionWithoutCustomProperties : Exception
    {
        public SerializableExceptionWithoutCustomProperties()
        {
        }

        public SerializableExceptionWithoutCustomProperties(string message) 
            : base(message)
        {
        }

        public SerializableExceptionWithoutCustomProperties(string message, Exception innerException) 
            : base(message, innerException)
        {
        }

        // Without this constructor, deserialization will fail
        protected SerializableExceptionWithoutCustomProperties(SerializationInfo info, StreamingContext context) 
            : base(info, context)
        {
        }
    }
}

Full implementation, with custom properties

Complete implementation of a custom serializable exception (MySerializableException), and a derived sealed exception (MyDerivedSerializableException).

The main points about this implementation are summarized here:

  1. You must decorate each derived class with the [Serializable] attribute — This attribute is not inherited from the base class, and if it is not specified, serialization will fail with a SerializationException stating that "Type X in Assembly Y is not marked as serializable."
  2. You must implement custom serialization. The [Serializable] attribute alone is not enough — Exception implements ISerializable which means your derived classes must also implement custom serialization. This involves two steps:
    1. Provide a serialization constructor. This constructor should be private if your class is sealed, otherwise it should be protected to allow access to derived classes.
    2. Override GetObjectData() and make sure you call through to base.GetObjectData(info, context) at the end, in order to let the base class save its own state.

SerializableExceptionWithCustomProperties.cs:

namespace SerializableExceptions
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using System.Security.Permissions;

    [Serializable]
    // Important: This attribute is NOT inherited from Exception, and MUST be specified 
    // otherwise serialization will fail with a SerializationException stating that
    // "Type X in Assembly Y is not marked as serializable."
    public class SerializableExceptionWithCustomProperties : Exception
    {
        private readonly string resourceName;
        private readonly IList<string> validationErrors;

        public SerializableExceptionWithCustomProperties()
        {
        }

        public SerializableExceptionWithCustomProperties(string message) 
            : base(message)
        {
        }

        public SerializableExceptionWithCustomProperties(string message, Exception innerException)
            : base(message, innerException)
        {
        }

        public SerializableExceptionWithCustomProperties(string message, string resourceName, IList<string> validationErrors)
            : base(message)
        {
            this.resourceName = resourceName;
            this.validationErrors = validationErrors;
        }

        public SerializableExceptionWithCustomProperties(string message, string resourceName, IList<string> validationErrors, Exception innerException)
            : base(message, innerException)
        {
            this.resourceName = resourceName;
            this.validationErrors = validationErrors;
        }

        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        // Constructor should be protected for unsealed classes, private for sealed classes.
        // (The Serializer invokes this constructor through reflection, so it can be private)
        protected SerializableExceptionWithCustomProperties(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            this.resourceName = info.GetString("ResourceName");
            this.validationErrors = (IList<string>)info.GetValue("ValidationErrors", typeof(IList<string>));
        }

        public string ResourceName
        {
            get { return this.resourceName; }
        }

        public IList<string> ValidationErrors
        {
            get { return this.validationErrors; }
        }

        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
            {
                throw new ArgumentNullException("info");
            }

            info.AddValue("ResourceName", this.ResourceName);

            // Note: if "List<T>" isn't serializable you may need to work out another
            //       method of adding your list, this is just for show...
            info.AddValue("ValidationErrors", this.ValidationErrors, typeof(IList<string>));

            // MUST call through to the base class to let it save its own state
            base.GetObjectData(info, context);
        }
    }
}

DerivedSerializableExceptionWithAdditionalCustomProperties.cs:

namespace SerializableExceptions
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using System.Security.Permissions;

    [Serializable]
    public sealed class DerivedSerializableExceptionWithAdditionalCustomProperty : SerializableExceptionWithCustomProperties
    {
        private readonly string username;

        public DerivedSerializableExceptionWithAdditionalCustomProperty()
        {
        }

        public DerivedSerializableExceptionWithAdditionalCustomProperty(string message)
            : base(message)
        {
        }

        public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, Exception innerException) 
            : base(message, innerException)
        {
        }

        public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, string username, string resourceName, IList<string> validationErrors) 
            : base(message, resourceName, validationErrors)
        {
            this.username = username;
        }

        public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, string username, string resourceName, IList<string> validationErrors, Exception innerException) 
            : base(message, resourceName, validationErrors, innerException)
        {
            this.username = username;
        }

        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        // Serialization constructor is private, as this class is sealed
        private DerivedSerializableExceptionWithAdditionalCustomProperty(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            this.username = info.GetString("Username");
        }

        public string Username
        {
            get { return this.username; }
        }

        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
            {
                throw new ArgumentNullException("info");
            }
            info.AddValue("Username", this.username);
            base.GetObjectData(info, context);
        }
    }
}

Unit Tests

MSTest unit tests for the three exception types defined above.

UnitTests.cs:

namespace SerializableExceptions
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class UnitTests
    {
        private const string Message = "The widget has unavoidably blooped out.";
        private const string ResourceName = "Resource-A";
        private const string ValidationError1 = "You forgot to set the whizz bang flag.";
        private const string ValidationError2 = "Wally cannot operate in zero gravity.";
        private readonly List<string> validationErrors = new List<string>();
        private const string Username = "Barry";

        public UnitTests()
        {
            validationErrors.Add(ValidationError1);
            validationErrors.Add(ValidationError2);
        }

        [TestMethod]
        public void TestSerializableExceptionWithoutCustomProperties()
        {
            Exception ex =
                new SerializableExceptionWithoutCustomProperties(
                    "Message", new Exception("Inner exception."));

            // Save the full ToString() value, including the exception message and stack trace.
            string exceptionToString = ex.ToString();

            // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter
            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                // "Save" object state
                bf.Serialize(ms, ex);

                // Re-use the same stream for de-serialization
                ms.Seek(0, 0);

                // Replace the original exception with de-serialized one
                ex = (SerializableExceptionWithoutCustomProperties)bf.Deserialize(ms);
            }

            // Double-check that the exception message and stack trace (owned by the base Exception) are preserved
            Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()");
        }

        [TestMethod]
        public void TestSerializableExceptionWithCustomProperties()
        {
            SerializableExceptionWithCustomProperties ex = 
                new SerializableExceptionWithCustomProperties(Message, ResourceName, validationErrors);

            // Sanity check: Make sure custom properties are set before serialization
            Assert.AreEqual(Message, ex.Message, "Message");
            Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName");
            Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count");
            Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]");
            Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]");

            // Save the full ToString() value, including the exception message and stack trace.
            string exceptionToString = ex.ToString();

            // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter
            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                // "Save" object state
                bf.Serialize(ms, ex);

                // Re-use the same stream for de-serialization
                ms.Seek(0, 0);

                // Replace the original exception with de-serialized one
                ex = (SerializableExceptionWithCustomProperties)bf.Deserialize(ms);
            }

            // Make sure custom properties are preserved after serialization
            Assert.AreEqual(Message, ex.Message, "Message");
            Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName");
            Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count");
            Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]");
            Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]");

            // Double-check that the exception message and stack trace (owned by the base Exception) are preserved
            Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()");
        }

        [TestMethod]
        public void TestDerivedSerializableExceptionWithAdditionalCustomProperty()
        {
            DerivedSerializableExceptionWithAdditionalCustomProperty ex = 
                new DerivedSerializableExceptionWithAdditionalCustomProperty(Message, Username, ResourceName, validationErrors);

            // Sanity check: Make sure custom properties are set before serialization
            Assert.AreEqual(Message, ex.Message, "Message");
            Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName");
            Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count");
            Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]");
            Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]");
            Assert.AreEqual(Username, ex.Username);

            // Save the full ToString() value, including the exception message and stack trace.
            string exceptionToString = ex.ToString();

            // Round-trip the exception: Serialize and de-serialize with a BinaryFormatter
            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                // "Save" object state
                bf.Serialize(ms, ex);

                // Re-use the same stream for de-serialization
                ms.Seek(0, 0);

                // Replace the original exception with de-serialized one
                ex = (DerivedSerializableExceptionWithAdditionalCustomProperty)bf.Deserialize(ms);
            }

            // Make sure custom properties are preserved after serialization
            Assert.AreEqual(Message, ex.Message, "Message");
            Assert.AreEqual(ResourceName, ex.ResourceName, "ex.ResourceName");
            Assert.AreEqual(2, ex.ValidationErrors.Count, "ex.ValidationErrors.Count");
            Assert.AreEqual(ValidationError1, ex.ValidationErrors[0], "ex.ValidationErrors[0]");
            Assert.AreEqual(ValidationError2, ex.ValidationErrors[1], "ex.ValidationErrors[1]");
            Assert.AreEqual(Username, ex.Username);

            // Double-check that the exception message and stack trace (owned by the base Exception) are preserved
            Assert.AreEqual(exceptionToString, ex.ToString(), "ex.ToString()");
        }
    }
}
Duncan Jones
  • 59,308
  • 24
  • 169
  • 227
Daniel Fortunov
  • 38,854
  • 23
  • 76
  • 101
  • 3
    +1: but if you're going to this much trouble, I'd go all the way and follow all the MS guidelines for implementing exceptions. One I can remember is to provide the standard construcors MyException(), MyException(string message) and MyException(string message, Exception innerException) – Joe Sep 19 '08 at 08:19
  • Standard exception constructors added. I also added a guard clause to thrown ArgumentNullException if the info parameter to GetObjectData is null. – Daniel Fortunov Sep 19 '08 at 09:36
  • Where are the "guidelines" that say I must implement the "standard" constructors? Are those ctors related to the correctness of the serializability? In other words, *for serializable*, must I implement the default ctor? The embedded exception ctor? – Cheeso Jul 01 '09 at 08:25
  • 4
    Also - that the Framework Design Guideliness say that names for exceptions *should* end with "Exception". Something like MyExceptionAndHereIsaQualifyingAdverbialPhrase is disrecommended. http://msdn.microsoft.com/en-us/library/ms229064.aspx Someone once said, the code we provide here is often used as a pattern, so we should be careful to get it right. – Cheeso Jul 01 '09 at 08:42
  • 1
    Cheeso: The book "Framework Design Guidelines", in the section on Designing Custom Exceptions, states: "Do provide (at least) these common constructors on all exceptions." See here: http://blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Only the (SerializationInfo info, StreamingContext context) constructor is needed for serialisation correctness, the rest is provided to make this a good starting point for cut-and-paste. When you cut and paste, however, you will surely change the class names, therefore I don't think violating the exception naming convention is significant here... – Daniel Fortunov Jul 02 '09 at 10:52
  • This answer (while excellent) doesn't take versioning into consideration. Take a look at http://stackoverflow.com/questions/2613874/iserializable-and-backward-compatibility . – Jared Moore Sep 28 '11 at 18:56
  • I don't get the difference between the with custom properties and with additional custom properties? Why is the first not sealed, and the second is? And also, why two example one with just more properties? – Didier A. May 08 '13 at 20:31
  • For the `[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]` part, see [Security and Serialization on MSDN](https://msdn.microsoft.com/en-us/library/ek7af9ck.aspx). – Palec Mar 18 '17 at 20:33
  • 5
    is this accepted answer true for .NET Core as well? In .net core `GetObjectData` never gets invoked..however i can override `ToString()` which gets Invoked – LP13 Mar 02 '18 at 00:10
  • 6
    It seems that this is not they way it is done in the new world. For example, literally no exception in ASP.NET Core is implemented this way. They all omit the serialization stuff: https://github.com/aspnet/Mvc/blob/d9825d1547e51619c0e4d6eba710c1f67172e136/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterException.cs – bitbonk Mar 12 '18 at 09:51
  • `System.Net.Sockets.SocketException` and `System.Net.Http.HttpRequestException` ***NOT SERIALIZABLE*** ? – Kiquenet Mar 13 '18 at 08:30
  • An update for the earlier comments - .NET Core supports binary serialization with a subset of types, including Exception and several "important Exception-derived types", which can be found [here](https://docs.microsoft.com/en-us/dotnet/standard/serialization/binary-serialization#serializable-types). The reasoning for the changes can be found here: [Scale back Serializable for .NET Core 2.0](https://github.com/dotnet/corefx/issues/19119#issue-225189643) – csrowell Oct 18 '18 at 14:29
  • Maybe want to avoid using `SecurityPermissionAttribute` these days ... https://docs.microsoft.com/en-us/dotnet/framework/misc/security-and-serialization – bytedev Jan 21 '21 at 01:34
30

Exception is already serializable, but you need to override the GetObjectData method to store your variables and provide a constructor which can be called when re-hydrating your object.

So your example becomes:

[Serializable]
public class MyException : Exception
{
    private readonly string resourceName;
    private readonly IList<string> validationErrors;

    public MyException(string resourceName, IList<string> validationErrors)
    {
        this.resourceName = resourceName;
        this.validationErrors = validationErrors;
    }

    public string ResourceName
    {
        get { return this.resourceName; }
    }

    public IList<string> ValidationErrors
    {
        get { return this.validationErrors; }
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    protected MyException(SerializationInfo info, StreamingContext context) : base (info, context)
    {
        this.resourceName = info.GetString("MyException.ResourceName");
        this.validationErrors = info.GetValue("MyException.ValidationErrors", typeof(IList<string>));
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);

        info.AddValue("MyException.ResourceName", this.ResourceName);

        // Note: if "List<T>" isn't serializable you may need to work out another
        //       method of adding your list, this is just for show...
        info.AddValue("MyException.ValidationErrors", this.ValidationErrors, typeof(IList<string>));
    }

}
CountZero
  • 5,431
  • 3
  • 41
  • 56
Adrian Clark
  • 12,189
  • 5
  • 32
  • 41
  • 1
    Often you can get away with just adding [Serializable] to you class. – Hallgrim Sep 18 '08 at 18:09
  • 4
    Hallgrim: Adding [Serializable] is not enough if you have additional fields to serialize. – Joe Sep 18 '08 at 18:40
  • 2
    NB: "In general this constructor should be protected if the class is not sealed" -- so the serialization constructor in your example should be protected (or, perhaps more appropriately, the class should be sealed unless inheritance is specifically required). Other than that, good work! – Daniel Fortunov Sep 18 '08 at 22:27
  • Two other mistakes in this: [Serializable] attribute is mandatory otherwise serialization fails; GetObjectData must call through to base.GetObjectData – Daniel Fortunov Sep 19 '08 at 07:29
10

To add to the correct answers above, I discovered that I can avoid doing this custom serialization stuff if I store my custom properties in the Data collection of the Exception class.

E.g.:

[Serializable]
public class JsonReadException : Exception
{
    // ...

    public string JsonFilePath
    {
        get { return Data[@"_jsonFilePath"] as string; }
        private set { Data[@"_jsonFilePath"] = value; }
    }

    public string Json
    {
        get { return Data[@"_json"] as string; }
        private set { Data[@"_json"] = value; }
    }

    // ...
}

Probably this is less efficient in terms of performance than the solution provided by Daniel and probably only works for "integral" types like strings and integers and the like.

Still it was very easy and very understandable for me.

Community
  • 1
  • 1
Uwe Keim
  • 36,867
  • 50
  • 163
  • 268
  • 2
    This is a nice and simple way to handle additional exception information in the case where you only need to store it for logging or something like that. If you ever needed to access these additional values in code in a catch-block however you would then be relying on knowing the keys for the data values externally which is not good for encapsulation etc. – Christopher King Mar 06 '15 at 20:45
  • 2
    Wow, thank you. I kept randomly losing all my custom added variables whenever an exception was rethrown using `throw;` and this fixed it. – Nyerguds Feb 26 '16 at 12:27
  • 1
    @ChristopherKing Why would you need to know the keys? They're hardcoded in the getter. – Nyerguds Feb 26 '16 at 12:31
10

Implement ISerializable, and follow the normal pattern for doing this.

You need to tag the class with the [Serializable] attribute, and add support for that interface, and also add the implied constructor (described on that page, search for implies a constructor). You can see an example of its implementation in the code below the text.

Lasse V. Karlsen
  • 350,178
  • 94
  • 582
  • 779
2

There used to be an excellent article from Eric Gunnerson on MSDN "The well-tempered exception" but it seems to have been pulled. The URL was:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp08162001.asp

Aydsman's answer is correct, more info here:

http://msdn.microsoft.com/en-us/library/ms229064.aspx

I can't think of any use-case for an Exception with non-serializable members, but if you avoid attempting to serialize/deserialize them in GetObjectData and the deserialization constructor you should be OK. Also mark them with the [NonSerialized] attribute, more as documentation than anything else, since you are implementing the serialization yourself.

Joe
  • 114,633
  • 27
  • 187
  • 321
0

In .NET Core, .Net 5.0 and above do not use Serializable because Microsoft follows the security threat practices found in BinaryFormatter.

Use the example storing in Data Collection

  • No, it is wrong. As per the Microsoft documentation :- http://msdn.microsoft.com/en-us/library/ms229064.aspx – Xenikh Mar 20 '21 at 09:35
  • @Xenikh - you're referencing ancient documentation (2013). @user2205317 - can you you point to any official docs that talk about the exception serialization pattern being deprecated? ASP.NET 5.0 code included Exceptions that do and do not include `Serializable`, eg: https://github.com/dotnet/aspnetcore/blob/v5.0.4/src/Http/Routing/src/Patterns/RoutePatternException.cs https://github.com/dotnet/aspnetcore/blob/v5.0.4/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CompilationFailedException.cs – crimbo Mar 26 '21 at 17:04
  • If Microsoft notifies there is a security problem (injection threats) in BinaryFormatter, SoapFormatter, LosFormatter, NetDataContractSerializer, ObjectStateFormatter, which consumes Serializable there are no formatters left in the BCL framework which support this pattern. Looking for a reason to support it. Recommendation is to use and alternative like XML, JSON, YMAL... – user2205317 Apr 14 '21 at 13:14
0

Mark the class with [Serializable], although I'm not sure how well a IList member will be handled by the serializer.

EDIT

The post below is correct, because your custom exception has constructor that takes parameters, you must implement ISerializable.

If you used a default constructor and exposed the two custom members with getter/setter properties, you could get away with just setting the attribute.

David Hill
  • 4,062
  • 2
  • 21
  • 18
-5

I have to think that wanting to serialize an exception is a strong indication that you're taking the wrong approach to something. What's the ultimate goal, here? If you're passing the exception between two processes, or between separate runs of the same process, then most of the properties of the exception aren't going to be valid in the other process anyway.

It would probably make more sense to extract the state information you want at the catch() statement, and archive that.

Mark Bessey
  • 19,065
  • 4
  • 44
  • 66
  • 9
    Downvote - the Microsoft guidelines state exceptions should be serializable http://msdn.microsoft.com/en-us/library/ms229064.aspx So they can be thrown across an appdomain boundary, e.g. using remoting. – Joe Sep 18 '08 at 18:50