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

时间:2008-09-18 17:03:33

标签: .net exception serialization

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

举个例子:

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

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

8 个答案:

答案 0 :(得分:363)

基本实现,没有自定义属性

<强> 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)
        {
        }
    }
}

完全实施,具有自定义属性

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

此处总结了有关此实施的要点:

  1. 必须使用[Serializable]属性修饰每个派生类 - 此属性不是从基类继承的,如果未指定,则序列化将失败,并且SerializationException表示“程序集Y中的类型X未标记为可序列化。” < / LI>
  2. 必须实施自定义序列化。仅[Serializable]属性是不够的 - Exception实现ISerializable,这意味着派生类还必须实现自定义序列化。这涉及两个步骤:
    1. 提供序列化构造函数。如果您的类为private,则此构造函数应为sealed,否则应为protected以允许访问派生类。
    2. 覆盖GetObjectData(),并确保在最后调用base.GetObjectData(info, context),以便让基类保存自己的状态。
  3. <强> 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);
            }
        }
    }
    

    单元测试

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

    <强> 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()");
            }
        }
    }
    

答案 1 :(得分:18)

异常已经可序列化,但您需要覆盖GetObjectData方法来存储变量,并提供一个构造函数,可以在重新保湿对象时调用它。

所以你的例子变成了:

[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>));
    }

}

答案 2 :(得分:7)

实施ISerializable,并按照normal pattern执行此操作。

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

答案 3 :(得分:5)

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

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; }
    }

    // ...
}

在性能方面,这可能比the solution provided by Daniel效率低,并且可能仅适用于&#34;积分&#34;类似字符串和整数等。

对我来说,这仍然很容易理解。

答案 4 :(得分:1)

曾经有一篇来自Eric Gunnerson在MSDN上发表的精彩文章“脾气暴躁的例外”,但它似乎已被撤下。网址是:

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

Aydsman的回答是正确的,更多信息在这里:

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

我无法想到使用非可序列化成员的异常的任何用例,但是如果您避免尝试在GetObjectData和反序列化构造函数中对它们进行序列化/反序列化,那么您应该没问题。同时使用[NonSerialized]属性标记它们,更多的是作为文档而不是其他任何内容,因为您自己实现了序列化。

答案 5 :(得分:0)

使用[Serializable]标记该类,但我不确定序列化程序将如何处理IList成员。

修改

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

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

答案 6 :(得分:0)

在 .NET Core 中,.Net 5.0 及更高版本不使用 Serializable,因为 Microsoft 遵循 BinaryFormatter 中的安全威胁做法。

使用示例存储在数据集合中

答案 7 :(得分:-5)

我必须认为想要序列化异常强烈表明你采取了错误的方法。这里的最终目标是什么?如果您在两个进程之间或同一进程的单独运行之间传递异常,那么异常的大部分属性无论如何都不会在另一个进程中有效。

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