¿Cuál es la forma correcta de hacer una excepción de .NET personalizada serializable?

Más específicamente, cuando la excepción contiene objetos personalizados que pueden o no ser serializables.

Toma este ejemplo:

public class MyException : Exception { private readonly string resourceName; private readonly IList validationErrors; public MyException(string resourceName, IList validationErrors) { this.resourceName = resourceName; this.validationErrors = validationErrors; } public string ResourceName { get { return this.resourceName; } } public IList ValidationErrors { get { return this.validationErrors; } } } 

Si esta excepción se serializa y deserializa, las dos propiedades personalizadas ( ResourceName y ValidationErrors ) no se conservarán. Las propiedades devolverán null .

¿Existe un patrón de código común para implementar la serialización para la excepción personalizada?

Implementación base, sin propiedades personalizadas

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) { } } } 

Implementación completa, con propiedades personalizadas

Implementación completa de una excepción serializable personalizada ( MySerializableException ) y una excepción sealed derivada ( MyDerivedSerializableException ).

Los puntos principales sobre esta implementación se resumen a continuación:

  1. Debe decorar cada clase derivada con el atributo [Serializable] : este atributo no se hereda de la clase base, y si no se especifica, la serialización fallará con una SerializationException que indica que “Type X en el conjunto Y no está marcado como serializable. “
  2. Debe implementar una serialización personalizada . El atributo [Serializable] sí solo no es suficiente: la Exception implementa ISerializable que significa que sus clases derivadas también deben implementar una serialización personalizada. Esto implica dos pasos:
    1. Proporcione un constructor de serialización . Este constructor debe ser private si su clase está sealed , de lo contrario debe estar protected para permitir el acceso a las clases derivadas.
    2. Anule GetObjectData () y asegúrese de llamar a base.GetObjectData(info, context) al final, para permitir que la clase base guarde su propio estado.

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 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 validationErrors) : base(message) { this.resourceName = resourceName; this.validationErrors = validationErrors; } public SerializableExceptionWithCustomProperties(string message, string resourceName, IList 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)info.GetValue("ValidationErrors", typeof(IList)); } public string ResourceName { get { return this.resourceName; } } public IList 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" 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)); // 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 validationErrors) : base(message, resourceName, validationErrors) { this.username = username; } public DerivedSerializableExceptionWithAdditionalCustomProperty(string message, string username, string resourceName, IList 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); } } } 

Pruebas unitarias

Pruebas unitarias MSTest para los tres tipos de excepciones definidos anteriormente.

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 validationErrors = new List(); 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()"); } } } 

La excepción ya es serializable, pero debe anular el método GetObjectData para almacenar sus variables y proporcionar un constructor al que se pueda llamar al rehidratar su objeto.

Entonces tu ejemplo se convierte en:

 [Serializable()] public class MyException : Exception { private readonly string resourceName; private readonly IList validationErrors; public MyException(string resourceName, IList validationErrors) { this.resourceName = resourceName; this.validationErrors = validationErrors; } public string ResourceName { get { return this.resourceName; } } public IList 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)); } [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" 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)); } } 

Implemente ISerializable y siga el patrón normal para hacer esto.

Debe etiquetar la clase con el atributo [Serializable] y agregar soporte para esa interfaz, y también agregar el constructor implícito (descrito en esa página, la búsqueda implica un constructor ). Puede ver un ejemplo de su implementación en el código debajo del texto.

Para agregar a las respuestas correctas anteriores, descubrí que puedo evitar hacer esta serialización personalizada si almaceno mis propiedades personalizadas en la colección de Data de la clase Exception .

P.ej:

 [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; } } // ... } 

Probablemente esto sea menos eficiente en términos de rendimiento que la solución provista por Daniel y probablemente solo funcione para tipos “integrales” como cadenas y números enteros y similares.

Aún así fue muy fácil y muy comprensible para mí.

Solía ​​haber un excelente artículo de Eric Gunnerson en MSDN “La excepción bien temperada”, pero parece haber sido retirado. La URL fue:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp08162001.asp

La respuesta de Aydsman es correcta, más información aquí:

http://msdn.microsoft.com/en-us/library/ms229064.aspx

No puedo pensar en ningún caso de uso para una Excepción con miembros no serializables, pero si evita intentar serializarlos / deserializarlos en GetObjectData y en el constructor de deserialización, debería estar bien. También márquelos con el atributo [NonSerialized], más como documentación que cualquier otra cosa, ya que usted mismo está implementando la serialización.

Marque la clase con [Serializable], aunque no estoy seguro de qué tan bien un miembro IList será manejado por el serializador.

EDITAR

La publicación a continuación es correcta, porque su excepción personalizada tiene un constructor que toma parámetros, debe implementar ISerializable.

Si utilizó un constructor predeterminado y expuso los dos miembros personalizados con las propiedades getter / setter, podría salirse con la simple configuración del atributo.

Tengo que pensar que querer serializar una excepción es una fuerte indicación de que estás tomando el enfoque equivocado de algo. ¿Cuál es el objective final, aquí? Si está pasando la excepción entre dos procesos, o entre ejecuciones separadas del mismo proceso, entonces la mayoría de las propiedades de la excepción no serán válidas en el otro proceso de todos modos.

Probablemente tenga más sentido extraer la información de estado que desee en la statement catch () y archivarla.