Quelle est la bonne façon de rendre une exception .NET personnalisée sérialisable?

225

Plus précisément, lorsque l'exception contient des objets personnalisés qui peuvent ou non être eux-mêmes sérialisables.

Prenez cet exemple:

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 cette exception est sérialisée et désérialisée, les deux propriétés personnalisées ( ResourceNameet ValidationErrors) ne seront pas conservées. Les propriétés reviendront null.

Existe-t-il un modèle de code commun pour implémenter la sérialisation pour l'exception personnalisée?

Daniel Fortunov
la source

Réponses:

411

Implémentation de base, sans propriétés personnalisées

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

Implémentation complète, avec des propriétés personnalisées

Implémentation complète d'une exception sérialisable personnalisée ( MySerializableException) et d'une sealedexception dérivée ( MyDerivedSerializableException).

Les principaux points concernant cette implémentation sont résumés ici:

  1. Vous devez décorer chaque classe dérivée avec l' [Serializable]attribut - Cet attribut n'est pas hérité de la classe de base et s'il n'est pas spécifié, la sérialisation échouera avec un SerializationExceptionmessage indiquant que «le type X dans l'assembly Y n'est pas marqué comme sérialisable».
  2. Vous devez implémenter la sérialisation personnalisée . L' [Serializable]attribut seul ne suffit pas - Exceptionimplémente, ISerializablece qui signifie que vos classes dérivées doivent également implémenter la sérialisation personnalisée. Cela implique deux étapes:
    1. Fournissez un constructeur de sérialisation . Ce constructeur devrait être privatesi votre classe l'est sealed, sinon il devrait être protectedpour permettre l'accès aux classes dérivées.
    2. Surchargez GetObjectData () et assurez-vous d'appeler base.GetObjectData(info, context)à la fin, afin de laisser la classe de base enregistrer son propre état.

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

Tests unitaires

Tests unitaires MSTest pour les trois types d'exceptions définis ci-dessus.

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
la source
3
+1: mais si vous rencontrez autant de problèmes, j'irais jusqu'au bout et suivrais toutes les directives MS pour la mise en œuvre des exceptions. Celui dont je me souviens est de fournir les constructeurs standard MyException (), MyException (message de chaîne) et MyException (message de chaîne, Exception innerException)
Joe
3
En outre - que la directive sur la conception du cadre indique que les noms des exceptions doivent se terminer par "Exception". Quelque chose comme MyExceptionAndHereIsaQualifyingAdverbialPhrase est déconseillé. msdn.microsoft.com/en-us/library/ms229064.aspx Quelqu'un a dit un jour, le code que nous fournissons ici est souvent utilisé comme modèle, nous devons donc faire attention de bien faire les choses.
Cheeso
1
Cheeso: Le livre "Framework Design Guidelines", dans la section Designing Custom Exceptions, stipule: "Fournissez (au moins) ces constructeurs communs à toutes les exceptions." Voir ici: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Seul le constructeur (SerializationInfo info, contexte StreamingContext) est nécessaire pour la correction de la sérialisation, le reste est fourni pour en faire un bon point de départ pour couper et coller. Cependant, lorsque vous coupez et collez, vous changez sûrement les noms de classe, donc je ne pense pas que la violation de la convention de dénomination des exceptions soit significative ici ...
Daniel Fortunov
3
cette réponse acceptée est-elle également vraie pour .NET Core? Dans le noyau .net, il GetObjectDatan'est jamais invoqué ... mais je peux remplacer ToString()ce qui est
invoqué
3
Il semble que ce n'est pas ainsi que cela se fait dans le nouveau monde. Par exemple, aucune exception dans ASP.NET Core n'est implémentée de cette façon. Ils omettent tous le truc de sérialisation: github.com/aspnet/Mvc/blob/…
bitbonk
25

L'exception est déjà sérialisable, mais vous devez remplacer la GetObjectDataméthode pour stocker vos variables et fournir un constructeur qui peut être appelé lors de la réhydratation de votre objet.

Votre exemple devient donc:

[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
la source
1
Souvent, vous pouvez vous en tirer en ajoutant simplement [Serializable] à votre classe.
Hallgrim
3
Hallgrim: l'ajout de [sérialisable] ne suffit pas si vous avez des champs supplémentaires à sérialiser.
Joe
2
NB: "En général, ce constructeur doit être protégé si la classe n'est pas scellée" - donc le constructeur de sérialisation dans votre exemple doit être protégé (ou, peut-être de façon plus appropriée, la classe doit être scellée sauf si l'héritage est spécifiquement requis). A part ça, bon travail!
Daniel Fortunov
Deux autres erreurs: l'attribut [Serializable] est obligatoire sinon la sérialisation échoue; GetObjectData doit appeler via base.GetObjectData
Daniel Fortunov
8

Implémentez ISerializable et suivez le modèle normal pour ce faire.

Vous devez étiqueter la classe avec l'attribut [Serializable] et ajouter la prise en charge de cette interface, et également ajouter le constructeur implicite (décrit sur cette page, la recherche implique un constructeur ). Vous pouvez voir un exemple de son implémentation dans le code sous le texte.

Lasse V. Karlsen
la source
8

Pour ajouter aux bonnes réponses ci-dessus, j'ai découvert que je peux éviter de faire ce truc de sérialisation personnalisé si je stocke mes propriétés personnalisées dans la Datacollection de la Exceptionclasse.

Par exemple:

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

    // ...
}

Ceci est probablement moins efficace en termes de performances que la solution fournie par Daniel et ne fonctionne probablement que pour les types "intégraux" comme les chaînes et les entiers et similaires.

C'était toujours très facile et très compréhensible pour moi.

Uwe Keim
la source
1
Il s'agit d'un moyen agréable et simple de gérer des informations d'exception supplémentaires dans le cas où vous n'avez besoin que de les stocker pour la journalisation ou quelque chose comme ça. Si jamais vous aviez besoin d'accéder à ces valeurs supplémentaires dans le code dans un bloc de capture, vous vous feriez alors compte de connaître les clés des valeurs de données en externe, ce qui n'est pas bon pour l'encapsulation, etc.
Christopher King
2
Wow merci. J'ai continué à perdre aléatoirement toutes mes variables ajoutées personnalisées chaque fois qu'une exception était renvoyée à l'aide de throw;ce problème .
Nyerguds
1
@ChristopherKing Pourquoi auriez-vous besoin de connaître les clés? Ils sont codés en dur dans le getter.
Nyerguds
1

Il y avait un excellent article d'Eric Gunnerson sur MSDN "L'exception bien tempérée" mais il semble avoir été retiré. L'URL était:

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

La réponse d'Aydsman est correcte, plus d'informations ici:

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

Je ne peux penser à aucun cas d'utilisation pour une exception avec des membres non sérialisables, mais si vous évitez d'essayer de les sérialiser / désérialiser dans GetObjectData et le constructeur de désérialisation, vous devriez être OK. Marquez-les également avec l'attribut [NonSerialized], plus comme documentation qu'autre chose, puisque vous implémentez vous-même la sérialisation.

Joe
la source
0

Marquez la classe avec [Serializable], même si je ne sais pas dans quelle mesure un membre IList sera géré par le sérialiseur.

ÉDITER

Le message ci-dessous est correct, car votre exception personnalisée a un constructeur qui prend des paramètres, vous devez implémenter ISerializable.

Si vous avez utilisé un constructeur par défaut et exposé les deux membres personnalisés avec des propriétés getter / setter, vous pouvez vous en sortir en définissant simplement l'attribut.

David Hill
la source
-5

Je dois penser que vouloir sérialiser une exception est une forte indication que vous adoptez la mauvaise approche pour quelque chose. Quel est le but ultime, ici? Si vous passez l'exception entre deux processus, ou entre des exécutions distinctes du même processus, la plupart des propriétés de l'exception ne seront pas valides de toute façon.

Il serait probablement plus logique d'extraire les informations d'état que vous souhaitez dans l'instruction catch () et de les archiver.

Mark Bessey
la source
9
Downvote - les consignes Microsoft stipulent que les exceptions doivent être sérialisables msdn.microsoft.com/en-us/library/ms229064.aspx Ainsi, elles peuvent être levées à travers une limite de domaine d'application, par exemple à l'aide de l'accès à distance.
Joe