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

Posted

技术标签:

【中文标题】使自定义 .NET 异常可序列化的正确方法是啥?【英文标题】:What is the correct way to make a custom .NET Exception serializable?使自定义 .NET 异常可序列化的正确方法是什么? 【发布时间】:2010-09-10 19:18:17 【问题描述】:

更具体地说,当异常包含可序列化或不可序列化的自定义对象时。

举个例子:

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

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

【问题讨论】:

【参考方案1】:

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

使用存储在Data Collection中的示例

【讨论】:

不,这是错误的。根据微软文档:- msdn.microsoft.com/en-us/library/ms229064.aspx @Xenikh - 您引用的是古代文献(2013 年)。 @ user2205317 - 你能指出任何官方文档讨论不推荐使用的异常序列化模式吗? ASP.NET 5.0 代码包含和不包含Serializable 的异常,例如:github.com/dotnet/aspnetcore/blob/v5.0.4/src/Http/Routing/src/… github.com/dotnet/aspnetcore/blob/v5.0.4/src/Mvc/… 如果 Microsoft 通知 BinaryFormatter、SoapFormatter、LosFormatter、NetDataContractSerializer、ObjectStateFormatter 中存在安全问题(注入威胁),它使用 Serializable,则 BCL 框架中没有支持此模式的格式化程序。寻找支持它的理由。建议使用 XML、JSON、YMAL 等替代方法...【参考方案2】:

Exception 已经是可序列化的,但您需要重写 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>));
    


【讨论】:

通常你只需将 [Serializable] 添加到你的类即可。 Hallgrim:如果您有额外的字段要序列化,添加 [Serializable] 是不够的。 注意:“一般来说,如果类未密封,则此构造函数应受到保护”-因此您示例中的序列化构造函数应受到保护(或者,也许更合适的是,该类应该被密封,除非继承是特别需要的)。除此之外,干得好! 这里还有两个错误: [Serializable] 属性是强制性的,否则序列化失败; GetObjectData 必须调用 base.GetObjectData【参考方案3】:

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

例如:

[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 有效,并且可能仅适用于字符串和整数等“整数”类型。

这对我来说还是很容易理解的。

【讨论】:

在您只需要存储它以进行日志记录或类似操作的情况下,这是一种处理附加异常信息的好方法和简单方法。如果您曾经需要在 catch-block 中的代码中访问这些附加值,那么您将依赖于知道外部数据值的键,这不利于封装等。 哇,谢谢。每当使用 throw; 重新抛出异常时,我都会随机丢失所有自定义添加的变量,这修复了它。 @ChristopherKing 为什么您需要知道密钥?它们在 getter 中被硬编码。【参考方案4】:

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

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) 的完整实现。

这里总结了这个实现的要点:

    必须使用[Serializable] 属性装饰每个派生类 — 该属性不是从基类继承的,如果未指定,序列化将失败并出现SerializationException 声明“程序集 Y 中的类型 X 未标记为可序列化。”必须实现自定义序列化。仅[Serializable] 属性是不够的——Exception 实现了ISerializable,这意味着您的派生类还必须实现自定义序列化。这包括两个步骤:
      提供序列化构造函数。如果您的类是 sealed,则此构造函数应为 private,否则应为 protected 以允许访问派生类。 重写 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()");
        
    

【讨论】:

+1:但是如果您遇到这么多麻烦,我会一直遵循所有 MS 指南来实施例外。我记得的一个是提供标准的构造函数 MyException(), MyException(string message) 和 MyException(string message, Exception innerException) 另外——框架设计指南说异常的名称应该以“Exception”结尾。不推荐使用 MyExceptionAndHereIsaQualifyingAdverbialPhrase 之类的东西。 msdn.microsoft.com/en-us/library/ms229064.aspx有人曾经说过,我们这里提供的代码经常被当成一种模式,所以要小心弄好。 Cheeso:“框架设计指南”一书在“设计自定义异常”部分中指出:“请在所有异常上提供(至少)这些通用构造函数。”请参阅此处:blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx 序列化正确性只需要 (SerializationInfo info, StreamingContext context) 构造函数,提供其余部分以使其成为剪切和粘贴的良好起点。但是,当您剪切和粘贴时,您肯定会更改类名,因此我认为在这里违反异常命名约定并不重要...... 这个公认的答案是否也适用于 .NET Core?在 .net 核心中 GetObjectData 永远不会被调用..但是我可以覆盖被调用的ToString() 似乎这不是他们在新世界中的做法。例如,从字面上看,ASP.NET Core 中的任何例外都是以这种方式实现的。他们都省略了序列化的东西:github.com/aspnet/Mvc/blob/…【参考方案5】:

MSDN 上曾经有 Eric Gunnerson 的一篇优秀文章《The well-tempered exception》,但是好像被撤了。网址是:

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

【讨论】:

【参考方案6】:

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

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

【讨论】:

Downvote - Microsoft 指南状态异常应该是可序列化的msdn.microsoft.com/en-us/library/ms229064.aspx 所以它们可以被抛出应用程序域边界,例如使用远程处理。【参考方案7】:

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

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

【讨论】:

【参考方案8】:

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

编辑

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

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

【讨论】:

以上是关于使自定义 .NET 异常可序列化的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 configureWebpack 使自定义变量可用于组件?

.Net 深度克隆——最好的方法是啥?

如何使自定义对象可迭代?

如何使自定义 UIView 可访问?

如何使自定义部分可执行(.text 除外)

Nativescript-vue:如何使自定义组件可点击?