使一个自定义的.NET异常可序列化

293
更具体地说,当异常包含可能是可序列化的自定义对象时,可能也可能不是。
以这个例子为例:
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; }
    }
}

如果这个异常被序列化和反序列化,那么两个自定义属性(ResourceName和ValidationErrors)将不会被保留。这些属性将返回null。
我该如何实现自定义异常的序列化?
8个回答

536

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

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,其中说明 "Type X in Assembly Y is not marked as serializable."
  2. 必须实现自定义序列化。仅有 [Serializable] 属性是不够的 — Exception 实现了 ISerializable 接口,这意味着你的派生类也必须实现自定义序列化。这包括两个步骤:
    1. 提供一个序列化构造函数。如果你的类是 sealed,则此构造函数应为 private,否则它应该是 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()");
        }
    }
}

5
如果你已经做了这么多工作,我建议你把它做到底,并遵循所有微软关于实现异常处理的准则。其中一个我记得是要提供标准构造函数 MyException()、MyException(string message) 和 MyException(string message, Exception innerException)。 - Joe
5
此外,框架设计指南规定,异常的名称应该以"Exception"结尾。类似"MyExceptionAndHereIsaQualifyingAdverbialPhrase"这样的命名方式是不被推荐的。有人曾经说过,我们提供的代码通常被用作模板,所以我们应该小心谨慎地做好它。 - Cheeso
8
这个被接受的答案是否也适用于.NET Core?在.NET Core中,GetObjectData方法从未被调用过,但我可以重写ToString()方法并使其被调用。 - LP13
16
在新的世界里,似乎不是这样做的。例如,在 ASP.NET Core 中,没有一个例外是以这种方式实现的。它们都省略了序列化的内容:https://github.com/aspnet/Mvc/blob/d9825d1547e51619c0e4d6eba710c1f67172e136/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterException.cs - bitbonk
4
现今也许最好避免使用“SecurityPermissionAttribute”... https://learn.microsoft.com/en-us/dotnet/framework/misc/security-and-serialization - bytedev
显示剩余8条评论

35

异常已经可以序列化,但是您需要重写 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
通常你只需要在类名上加 [Serializable] 就可以了。 - Hallgrim
7
Hallgrim:如果你有额外的字段需要序列化,仅添加[Serializable]是不够的。 - Joe
2
一般来说,如果类没有被密封,这个构造函数应该是受保护的。因此,在你的示例中,序列化构造函数应该是受保护的(或者更合适的是,除非特别需要继承,否则该类应该被密封)。除此之外,干得好! - Daniel Fortunov

14

补充上面正确的答案,我发现如果我把自定义属性存储在Exception类的Data集合中,我就可以避免进行自定义序列化操作。

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

    // ...
}

可能这种方法在性能方面不如Daniel提供的解决方案高效,而且可能只适用于像字符串和整数等“整型”类型。

但对我来说,它仍然非常简单易懂。


2
这是一种不错且简单的方式,用于处理仅需将其存储以进行日志记录等情况下的附加异常信息。但是,如果您需要在代码中的 catch 块中访问这些附加值,则会依赖于外部数据值的键,这对于封装性等方面并不好。 - Christopher King
2
哇,谢谢。每当使用 throw; 重新抛出异常时,我都会随机丢失所有自定义添加的变量,而这个问题现在已经解决了。 - Nyerguds
6
为什么你需要知道这些键呢?它们在getter函数中已经硬编码了。 - Nyerguds

10

实现ISerializable接口,并按照常规模式进行操作。

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


2
曾经有一篇Eric Gunnerson在MSDN上的优秀文章“The well-tempered exception”,但似乎已被撤下。URL为: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]属性将它们标记起来,更多的是文档说明而不是其他任何东西,因为您自己正在实现序列化。

2
在.NET Core中,.NET 5.0及以上版本不使用Serializable,因为Microsoft遵循BinaryFormatter中发现的安全威胁实践。请使用存储在数据集合中的示例。

不,这是错误的。根据微软文档:http://msdn.microsoft.com/en-us/library/ms229064.aspx - Abhishek Dutt
1
@Xenikh - 你引用了古老的文档(2013年)。@user2205317 - 你能指出任何官方文档,谈论异常序列化模式已被弃用吗?ASP.NET 5.0代码包括具有和不具有Serializable的异常,例如: 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
如果Microsoft通知BinaryFormatter、SoapFormatter、LosFormatter、NetDataContractSerializer和ObjectStateFormatter中存在安全问题(注入威胁),并且这些格式化程序使用Serializable,那么在BCL框架中将不再支持此模式的格式化程序。寻找支持它的理由。建议使用替代方案,如XML、JSON、YMAL等。 - user2205317

0

在类上标记[Serializable],尽管我不确定IList成员会被序列化程序处理得如何。

编辑

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

如果您使用默认构造函数并使用getter/setter属性公开了两个自定义成员,则可以仅设置属性来完成。


-5

我认为想要对异常进行序列化是表明您正在以错误的方式处理某些事情的强烈迹象。这里的最终目标是什么?如果您在两个进程之间或在同一进程的不同运行之间传递异常,那么异常的大多数属性在其他进程中都无效。

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


9
Downvote - 微软的指导方针说明例外情况应该是可序列化的,以便可以通过应用程序域边界进行传递,例如使用远程处理。 - Joe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接