Qual é a maneira correta de tornar uma Exceção .NET personalizada serializável?


225

Mais especificamente, quando a exceção contém objetos personalizados que podem ou não ser serializáveis.

Veja este exemplo:

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

Se essa exceção for serializada e desserializada, as duas propriedades personalizadas ( ResourceNamee ValidationErrors) não serão preservadas. As propriedades retornarão null.

Existe um padrão de código comum para implementar a serialização para exceção personalizada?

Respostas:


411

Implementação básica, sem propriedades customizadas

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

Implementação completa, com propriedades customizadas

Implementação completa de uma exceção serializável personalizada ( MySerializableException) e uma sealedexceção derivada ( MyDerivedSerializableException).

Os principais pontos sobre esta implementação estão resumidos aqui:

  1. Você deve decorar cada classe derivada com o [Serializable]atributo - Este atributo não é herdado da classe base e, se não for especificado, a serialização falhará com a SerializationExceptionindicação de que "o Tipo X no Conjunto Y não está marcado como serializável".
  2. Você deve implementar a serialização personalizada . O [Serializable]atributo sozinho não é suficiente - Exceptionimplementa, o ISerializableque significa que suas classes derivadas também devem implementar serialização customizada. Isso envolve duas etapas:
    1. Forneça um construtor de serialização . Esse construtor deve ser privatese sua classe for sealed, caso contrário, deve protectedpermitir o acesso a classes derivadas.
    2. Substitua GetObjectData () e certifique-se de chamar até base.GetObjectData(info, context)o final, para permitir que a classe base salve seu próprio 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);
        }
    }
}

Testes unitários

Testes de unidade MSTest para os três tipos de exceção definidos acima.

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()");
        }
    }
}

3
+1: mas se você estiver enfrentando tantos problemas, eu seguirei todo o caminho e seguirei todas as diretrizes da MS para implementar exceções. Uma que me lembro é fornecer o construcors MyException padrão (), MyException (mensagem de texto) e MyException (string message, Exception innerException)
Joe

3
Além disso - que a Diretriz de Design do Framework diga que os nomes das exceções devem terminar com "Exceção". Algo como MyExceptionAndHereIsaQualifyingAdverbialPhrase é recomendado. msdn.microsoft.com/pt-br/library/ms229064.aspx Alguém disse uma vez que o código que fornecemos aqui é frequentemente usado como padrão; portanto, devemos ter cuidado para corrigi-lo.
Cheeso

1
Cheeso: O livro "Framework Design Guidelines", na seção Designing Custom Exceptions, afirma: "Forneça (pelo menos) esses construtores comuns em todas as exceções". Consulte aqui: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Somente o construtor (informações SerializationInfo, contexto StreamingContext) é necessário para a correção da serialização, o restante é fornecido para torná-lo um bom ponto de partida para copiar e colar. Quando você cortar e colar, no entanto, você certamente irá mudar os nomes de classe, portanto, eu não acho que violar a convenção de nomenclatura exceção é significativo aqui ...
Daniel Fortunov

3
esta resposta aceita também é verdadeira para o .NET Core? No núcleo .net GetObjectDatanunca fica invoked..however i pode substituir ToString()o que é invocado
LP13

3
Parece que não é assim que se faz no novo mundo. Por exemplo, literalmente, nenhuma exceção no ASP.NET Core é implementada dessa maneira. Todos eles omitem o material de serialização: github.com/aspnet/Mvc/blob/…
bitbonk

25

A exceção já é serializável, mas você precisa substituir o GetObjectDatamétodo para armazenar suas variáveis ​​e fornecer um construtor que pode ser chamado ao reidratar seu objeto.

Portanto, seu exemplo se torna:

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

}

1
Muitas vezes você pode simplesmente adicionar [Serializable] à sua turma.
Hallgrim

3
Hallgrim: Adicionar [Serializable] não é suficiente se você tiver campos adicionais para serializar.
Joe

2
NB: "Em geral, esse construtor deve ser protegido se a classe não for selada" - portanto, o construtor de serialização no seu exemplo deve ser protegido (ou, talvez mais apropriadamente, a classe deve ser selada, a menos que a herança seja especificamente necessária). Fora isso, bom trabalho!
18730 Daniel Fortunov

Dois outros erros nisto: o atributo [Serializable] é obrigatório, caso contrário, a serialização falha; GetObjectData deve chamar até base.GetObjectData
Daniel Fortunov

8

Implemente ISerializable e siga o padrão normal para fazer isso.

Você precisa marcar a classe com o atributo [Serializable], adicionar suporte a essa interface e também adicionar o construtor implícito (descrito nessa página, procurar implica em um construtor ). Você pode ver um exemplo de sua implementação no código abaixo do texto.


8

Para adicionar as respostas corretas acima, descobri que posso evitar fazer esse material de serialização personalizado se armazenar minhas propriedades personalizadas na Datacoleção da Exceptionclasse.

Por exemplo:

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

    // ...
}

Provavelmente, isso é menos eficiente em termos de desempenho do que a solução fornecida por Daniel e provavelmente só funciona para tipos "integrais", como seqüências de caracteres e números inteiros, etc.

Ainda assim, foi muito fácil e muito compreensível para mim.


1
Essa é uma maneira simples e agradável de lidar com informações de exceção adicionais no caso em que você só precisa armazená-las para log ou algo assim. Se você já precisou acessar esses valores adicionais no código em um bloco catch, contudo, você seria, então, depender de conhecer as chaves para os valores de dados externamente que não é bom para encapsulamento etc.
Christopher King

2
Uau, obrigada. Eu continuava perdendo aleatoriamente todas as minhas variáveis ​​adicionadas personalizadas sempre que uma exceção era repetida, throw;e isso era corrigido.
Nyerguds

1
@ChristopherKing Por que você precisaria conhecer as chaves? Eles estão codificados no código.
Nyerguds

1

Havia um excelente artigo de Eric Gunnerson no MSDN "A exceção bem-humorada", mas parece ter sido puxado. O URL era:

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

A resposta de Aydsman está correta, mais informações aqui:

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

Não consigo pensar em nenhum caso de uso para uma exceção com membros não serializáveis, mas se você evitar tentar serializá-los / desserializá-los em GetObjectData e no construtor de desserialização, deverá estar OK. Marque-os também com o atributo [NonSerialized], mais como documentação do que qualquer outra coisa, pois você mesmo está implementando a serialização.


0

Marque a classe com [Serializable], embora eu não tenha certeza de quão bem um membro do IList será tratado pelo serializador.

EDITAR

A postagem abaixo está correta, porque sua exceção personalizada possui um construtor que aceita parâmetros, você deve implementar ISerializable.

Se você usasse um construtor padrão e expusesse os dois membros personalizados com propriedades getter / setter, seria possível definir apenas o atributo


-5

Eu tenho que pensar que querer serializar uma exceção é uma forte indicação de que você está adotando a abordagem errada de algo. Qual é o objetivo final, aqui? Se você estiver passando a exceção entre dois processos ou entre execuções separadas do mesmo processo, a maioria das propriedades da exceção não será válida no outro processo.

Provavelmente faria mais sentido extrair as informações de estado que você deseja na instrução catch () e arquivá-las.


9
Votos negativos - as exceções de estado das diretrizes da Microsoft devem ser serializáveis msdn.microsoft.com/en-us/library/ms229064.aspx Para que possam ser lançadas através de um limite de domínio de aplicativo, por exemplo, usando o sistema de comunicação remota.
Joe
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.