什么是正确的方式来使一个自定义的.NETexception序列化?
更具体地说,当exception包含可能或不可以序列化的自定义对象时。
以这个例子:
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被序列化和反序列化,那么这两个自定义属性( ResourceName
和ValidationErrors
)将不会被保留。 该属性将返回null
。
是否有一个常见的代码模式来实现自定义exception序列化?
基本实现,没有自定义属性
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) { } } }
完全实现,具有自定义属性
完成自定义可序列化exception( MySerializableException
)的实现以及派生sealed
exception( MyDerivedSerializableException
)。
这里总结了这个实现的要点:
- 您必须使用
[Serializable]
属性修饰每个派生类 – 此属性不是从基类inheritance的,如果未指定,则序列化将失败,并显示SerializationException
指出“Assembly Y中的Type 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); } } }
unit testing
MSTestunit testing以上定义的三种exceptiontypes。
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()"); } } }
exception已经是可序列化的,但是你需要重写GetObjectData
方法来存储你的variables,并提供一个构造函数,当你重新水化你的对象时可以调用它。
所以你的例子变成:
[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>)); } }
实现ISerializable,并按照正常的模式来做到这一点。
您需要使用[Serializable]属性标记类,并添加对该接口的支持,并添加隐含的构造函数(在该页面上描述,search隐含的构造函数 )。 您可以在文本下面的代码中看到它的实现示例。
要添加到上面的正确答案,我发现,如果我将自定义属性存储在Exception
类的Data
集合中,我可以避免执行此自定义序列化的东西。
例如:
[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所提供的解决scheme效率低,可能只适用于“整数”types,比如string和整数等等。
对我来说,这仍然是非常容易和可以理解的。
曾经有一篇来自MSDN的Eric Gunnerson的优秀文章“脾气暴躁的例外”,但似乎已被拉下。 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]属性标记它们,更像是文档而不是其他任何东西,因为你正在自己实现序列化。
用[Serializable]标记类,虽然我不确定序列化程序如何处理IList成员。
编辑
下面的post是正确的,因为你的自定义exception具有参数的构造函数,你必须实现ISerializable。
如果你使用了一个默认的构造函数,并用getter / setter属性暴露了两个自定义成员,那么只需设置属性即可。
我不得不认为,想序列化一个exception是一个强烈的迹象,表明你正在采取错误的方法。 这里的最终目标是什么? 如果您在两个进程之间或同一进程的单独运行之间传递exception,那么exception的大部分属性在其他进程中都不会有效。
在catch()语句中提取所需的状态信息可能更有意义,并将其归档。