Dot-Net

使自定義 .NET 異常可序列化的正確方法是什麼?

  • September 18, 2008

更具體地說,當異常包含自定義對象時,這些對象本身可能是可序列化的,也可能不是。

舉個例子:

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

是否有用於實現自定義異常序列化的通用程式碼模式?

基本實現,沒有自定義屬性

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 未標記為可序列化”。*

  2. 必須實現自定義序列化[Serializable]僅有屬性是不夠的——實現ExceptionISerializable意味著您的派生類還必須實現自定義序列化。這包括兩個步驟:

  3. 提供一個序列化建構子private如果您的類是,則此建構子應該是sealed,否則應該是protected允許訪問派生類。

  4. **覆蓋 GetObjectData()**並確保base.GetObjectData(info, context)在最後呼叫 to,以便讓基類保存自己的狀態。

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 對上面定義的三種異常類型進行單元測試。

單元測試.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()");
       }
   }
}

引用自:https://stackoverflow.com/questions/94488