使自定义.NET Exception可序列化的正确方法是什么?

使自定义.NET Exception可序列化的正确方法是什么?

What is the correct way to make a custom .NET Exception serializable?

更具体地说,当异常包含自定义对象时,自定义对象本身可以序列化也可以不序列化。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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; }
    }
}

如果将此异常序列化和反序列化,则将不保留两个自定义属性(ResourceNameValidationErrors)。 这些属性将返回null

是否存在用于实现自定义异常的序列化的通用代码模式?


基本实现,无自定义属性

SerializableExceptionWithoutCustomProperties.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
        {
        }
    }
}

具有自定义属性的完整实现

自定义可序列化异常(MySerializableException)和派生sealed异常(MyDerivedSerializableException)的完整实现。

有关此实现的要点总结如下:

  • 您必须使用[Serializable]属性装饰每个派生类-
    此属性不是从基类继承的,如果未指定该属性,则序列化将失败,并显示SerializationException,指出"程序集Y中的类型X未标记为可序列化"。
  • 您必须实现自定义序列化。仅[Serializable]属性是不够的-Exception实现ISerializable,这意味着派生类还必须实现自定义序列化。这涉及两个步骤:

  • 提供序列化构造函数。如果您的类是sealed,则此构造函数应为private,否则应为protected,以允许访问派生类。
  • 重写GetObjectData()并确保最后调用base.GetObjectData(info, context),以使基类保存其自己的状态。
  • SerializableExceptionWithCustomProperties.cs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    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);
            }
        }
    }

    单元测试

    MSTest单元测试上面定义的三种异常类型。

    UnitTests.cs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    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()");
            }
        }
    }

    异常已经可以序列化,但是您需要重写GetObjectData方法来存储变量,并提供一个构造函数,该构造函数可以在重新为对象补水时调用。

    因此,您的示例变为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    [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>));
        }

    }

    为了补充上面的正确答案,我发现如果将自定义属性存储在Exception类的Data集合中,则可以避免执行此自定义序列化工作。

    例如。:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [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; }
        }

        // ...
    }

    就性能而言,这可能比Daniel提供的解决方案效率低,并且可能仅适用于"整数"类型,例如字符串和整数等。

    对于我来说,这仍然是非常容易并且可以理解的。


    实现ISerializable,并按照正常模式进行操作。

    您需要使用[Serializable]属性标记该类,并添加对该接口的支持,还需要添加隐式构造函数(在该页面上进行描述,搜索隐式构造函数)。您可以在文本下方的代码中看到其实现的示例。


    曾经有Eric Gunnerson在MSDN上发表的一篇精彩文章"脾气暴躁的异常",但它似乎已被取消。该网址为:

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

    艾德斯曼的答案是正确的,更多信息在这里:

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

    我想不出具有非可序列化成员的Exception的任何用例,但是如果您避免尝试在GetObjectData和反序列化构造函数中序列化/反序列化它们,则应该可以。还用[NonSerialized]属性标记它们,更多的是作为文档,而不是其他任何东西,因为您是自己实现序列化的。


    用[Serializable]标记类,尽管我不确定串行器将如何处理IList成员。

    编辑

    下面的帖子是正确的,因为您的自定义异常具有带参数的构造函数,因此您必须实现ISerializable。

    如果使用默认构造函数并使用getter / setter属性公开这两个自定义成员,则只需设置该属性即可。


    我必须认为,要序列化异常是一个很强的迹象,表明您对某些问题采用了错误的方法。最终目标是什么?如果要在两个进程之间或同一进程的不同运行之间传递异常,则该异常的大多数属性无论如何在另一个进程中将无效。

    在catch()语句中提取所需的状态信息并将其存档可能会更有意义。


    推荐阅读