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


224

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

举个例子:

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

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

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

Answers:


411

基本实现,无自定义属性

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中的Type X未标记为可序列化”。
  2. 必须实现自定义序列化。在[Serializable]单独的属性是不够的- Exception工具ISerializable,这意味着您的派生类还必须实现自定义序列。这涉及两个步骤:
    1. 提供序列化构造函数private如果您的类是sealed,则此构造函数应为,否则应protected允许访问派生类。
    2. 重写GetObjectData()并确保最后调用base.GetObjectData(info, context),以使基类保存其自己的状态。

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

3
+1:但是如果您要处理这么多麻烦,我会一直坚持下去,并遵循所有MS准则实施例外。我记得的一个方法是提供标准的构造函数MyException(),MyException(字符串消息)和MyException(字符串消息,Exception innerException)
Joe

3
另外-框架设计指南指出,例外名称以“ Exception”结尾。不建议使用MyExceptionAndHereIsaQualifyingAdverbialPhrase之类的东西。 msdn.microsoft.com/zh-cn/library/ms229064.aspx 有人曾经说过,我们在此处提供的代码通常被用作模式,因此我们应该小心正确。
Cheeso,2009年

1
Cheeso:“框架设计指南”一书在“设计自定义异常”部分中指出:“(至少)在所有异常上提供这些通用构造函数。” 参见此处:blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx 仅需要(SerializationInfo info,StreamingContext context)构造函数即可确保序列化的正确性,其余部分则是为了使之成为一个良好的起点剪切和粘贴。但是,当您剪切和粘贴时,您肯定会更改类名,因此,我认为在这里违反异常命名约定并不重要……
Daniel Fortunov,2009年

3
.NET Core是否也接受此答案?在.net核心中,GetObjectData永远不会被ToString()调用
..但是

3
看来这不是他们在新世界中所做的方式。例如,实际上,ASP.NET Core中没有异常是通过这种方式实现的。他们都省略了序列化内容:github.com/aspnet/Mvc/blob/…–
bitbonk

25

异常已经可以序列化,但是您需要重写该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>));
    }

}

1
通常,您只需在课程中添加[可序列化]就可以摆脱困境。
Hallgrim

3
Hallgrim:如果要序列化其他字段,添加[可序列化]还不够。

2
注意:“通常,如果不对类进行密封,则应保护此构造函数”-因此,应保护示例中的序列化构造函数(或更恰当的做法是,对类进行密封,除非明确要求继承)。除此之外,还不错!
Daniel Fortunov

这有两个错误:[Serializable]属性是必需的,否则序列化将失败;GetObjectData必须通过调用base.GetObjectData
Daniel Fortunov

8

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

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


8

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

例如:

[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提供的解决方案效率低并且可能仅适用于“整数”类型,例如字符串和整数等。

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


1
如果您只需要将其存储以进行日志记录或类似的操作,这是一种处理其他异常信息的好方法。如果你需要的代码访问在catch块,但是你然后将依托知道密钥的数据值从外部是不利于封装等这些附加价值
克里斯托弗·金

2
哇谢谢你。每当使用异常重新抛出异常时,我都会随机丢失所有自定义添加的变量,throw;并对此进行了修复。
Nyerguds '16

1
@ChristopherKing为什么您需要知道按键?它们在吸气剂中进行了硬编码。
Nyerguds '16

1

曾经有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]属性标记它们,更多的是作为文档,而不是其他任何东西,因为您是自己实现序列化的。


0

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

编辑

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

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


-5

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

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


9
下载-Microsoft准则状态例外应可序列化msdn.microsoft.com/zh-cn/library/ms229064.aspx ,以便可以将它们跨应用域边界抛出,例如使用远程处理。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.