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

225

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

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

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

Daniel Fortunov
fuente

Respuestas:

411

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 sealedexcepción derivada ( MyDerivedSerializableException).

Los puntos principales sobre esta implementación se resumen aquí:

  1. Usted debe decorar cada clase derivada con el [Serializable]atributo - Este atributo no se hereda de la clase base, y si no se especifica, se producirá un error de serialización con un SerializationExceptionafirmando que "tipo X en la Asamblea Y no está marcado como serializable."
  2. Usted debe implementar serialización personalizada . El [Serializable]atributo por sí solo no es suficiente: Exceptionimplementa lo ISerializableque significa que sus clases derivadas también deben implementar la serialización personalizada. Esto implica dos pasos:
    1. Proporcionar un constructor de serialización . Este constructor debería ser privatesi su clase es sealed, de lo contrario debería ser protectedpermitir el acceso a clases derivadas.
    2. Anule GetObjectData () y asegúrese de llamar al base.GetObjectData(info, context)final para 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<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);
        }
    }
}

Pruebas unitarias

Las pruebas unitarias MST para los tres tipos de excepción 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<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()");
        }
    }
}
Daniel Fortunov
fuente
3
+1: pero si te vas a meter en tantos problemas, seguiría todo el camino y seguiría todas las pautas de MS para implementar excepciones. Una que puedo recordar es proporcionar los constructores estándar MyException (), MyException (mensaje de cadena) y MyException (mensaje de cadena, Excepción innerException)
Joe
3
Además, que la Guía de diseño del marco dice que los nombres de las excepciones deben terminar con "Excepción". Algo como MyExceptionAndHereIsaQualifyingAdverbialPhrase no se recomienda. msdn.microsoft.com/en-us/library/ms229064.aspx Alguien dijo una vez, el código que proporcionamos aquí a menudo se usa como patrón, por lo que debemos tener cuidado de hacerlo bien.
Cheeso 01 de
1
Cheeso: El libro "Pautas de diseño del marco", en la sección sobre Diseño de excepciones personalizadas, establece: "Proporcione (al menos) estos constructores comunes en todas las excepciones". Ver aquí: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Solo el constructor (SerializationInfo info, StreamingContext context) es necesario para la corrección de la serialización, el resto se proporciona para hacer de este un buen punto de partida para cortar y pegar. Al cortar y pegar, sin embargo, seguramente cambiar los nombres de las clases, por lo tanto, no creo que viola la convención de nombres es una excepción significativa aquí ...
Daniel Fortunov
3
¿Es esta respuesta aceptada verdadera también para .NET Core? En .net core GetObjectDatanunca se ToString()invoca
embargo,
3
Parece que no es así como se hace en el nuevo mundo. Por ejemplo, literalmente, ninguna excepción en ASP.NET Core se implementa de esta manera. Todos omiten las cosas de serialización: github.com/aspnet/Mvc/blob/…
bitbonk
25

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

Entonces su ejemplo se convierte en:

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

}
Adrian Clark
fuente
1
A menudo puedes salirte con solo agregar [Serializable] a tu clase.
Hallgrim
3
Hallgrim: Agregar [Serializable] no es suficiente si tiene campos adicionales para serializar.
Joe
2
NB: "En general, este constructor debe estar protegido si la clase no está sellada", por lo que el constructor de serialización en su ejemplo debería estar protegido (o, quizás más apropiadamente, la clase debería estar sellada a menos que se requiera específicamente la herencia). Aparte de eso, ¡buen trabajo!
Daniel Fortunov
Otros dos errores en esto: el atributo [Serializable] es obligatorio; de lo contrario, la serialización falla; GetObjectData debe llamar a la base
Daniel Fortunov
8

Implemente ISerializable y siga el patrón normal para hacerlo.

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, buscar implica un constructor ). Puede ver un ejemplo de su implementación en el código debajo del texto.

Lasse V. Karlsen
fuente
8

Para agregar a las respuestas correctas anteriores, descubrí que puedo evitar hacer estas cosas de serialización personalizadas si almaceno mis propiedades personalizadas en la Datacolección de la Exceptionclase.

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 proporcionada por Daniel y probablemente solo funcione para tipos "integrales" como cadenas y enteros y similares.

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

Uwe Keim
fuente
1
Esta es una manera agradable y sencilla de manejar información adicional de excepciones en el caso de que solo necesite almacenarla para iniciar sesión o algo así. Si alguna vez necesita acceder a estos valores adicionales de código en un bloque catch sin embargo, a continuación, estaría confiando en conocer las claves de los valores de los datos externamente que no es bueno para la encapsulación etc
Christopher King
2
Wow gracias. Seguí perdiendo al azar todas mis variables agregadas personalizadas cada vez que se volvía a lanzar una excepción throw;y esto lo solucionó.
Nyerguds
1
@ChristopherKing ¿Por qué necesitarías saber las llaves? Están codificados en el captador.
Nyerguds
1

Solía ​​haber un excelente artículo de Eric Gunnerson en MSDN "La excepción de mal genio", 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 el constructor de deserialización, debería estar bien. También márquelos con el atributo [No serializado], más como documentación que otra cosa, ya que está implementando la serialización usted mismo.

Joe
fuente
0

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

EDITAR

La publicación a continuación es correcta, ya que 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 propiedades getter / setter, podría salirse con la suya simplemente configurando el atributo.

David Hill
fuente
-5

Tengo que pensar que querer serializar una excepción es una fuerte indicación de que estás tomando el enfoque incorrecto de algo. ¿Cuál es el objetivo 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 tendría más sentido extraer la información de estado que desea en la instrucción catch () y archivarla.

Mark Bessey
fuente
99
Voto a favor: las excepciones del estado de las pautas de Microsoft deben ser serializables msdn.microsoft.com/en-us/library/ms229064.aspx Para que puedan arrojarse a través de un límite de dominio de aplicación, por ejemplo, utilizando la comunicación remota.
Joe