Qual è il modo corretto di rendere serializzabile un'eccezione .NET personalizzata?


225

Più specificamente, quando l'eccezione contiene oggetti personalizzati che possono o meno essere serializzabili.

Prendi questo esempio:

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 questa eccezione è serializzata e deserializzata, le due proprietà personalizzate ( ResourceNamee ValidationErrors) non verranno conservate. Le proprietà torneranno null.

Esiste un modello di codice comune per l'implementazione della serializzazione per l'eccezione personalizzata?

Risposte:


411

Implementazione di base, senza proprietà personalizzate

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

Implementazione completa, con proprietà personalizzate

Implementazione completa di un'eccezione serializzabile personalizzata ( MySerializableException) e sealedun'eccezione derivata ( MyDerivedSerializableException).

I punti principali di questa implementazione sono riassunti qui:

  1. È necessario decorare ogni classe derivata con l' [Serializable]attributo - Questo attributo non è ereditato dalla classe base e, se non è specificato, la serializzazione fallirà SerializationExceptionaffermando che "Tipo X nell'assemblaggio Y non è contrassegnato come serializzabile".
  2. È necessario implementare la serializzazione personalizzata . L' [Serializable]attributo da solo non è sufficiente: Exceptionimplementa il ISerializableche significa che anche le tue classi derivate devono implementare la serializzazione personalizzata. Ciò comporta due passaggi:
    1. Fornire un costruttore di serializzazione . Questo costruttore dovrebbe essere privatese la tua classe lo è sealed, altrimenti dovrebbe essere protectedper consentire l'accesso alle classi derivate.
    2. Sostituisci GetObjectData () e assicurati di chiamare fino base.GetObjectData(info, context)alla fine, per consentire alla classe base di salvare il proprio stato.

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

Test unitari

MSTest unit test per i tre tipi di eccezione definiti sopra.

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: ma se hai così tanti problemi, andrei fino in fondo e seguirò tutte le linee guida MS per l'implementazione delle eccezioni. Ricordo che è possibile fornire i costruttori standard MyException (), MyException (messaggio stringa) e MyException (messaggio stringa, Eccezione innerException)
Joe,

3
Inoltre - che la guida alla progettazione del framework afferma che i nomi delle eccezioni dovrebbero terminare con "Eccezione". Qualcosa come MyExceptionAndHereIsaQualifyingAdverbialPhrase è sconsigliato. msdn.microsoft.com/en-us/library/ms229064.aspx Qualcuno una volta ha detto, il codice che forniamo qui è spesso usato come modello, quindi dovremmo fare attenzione a farlo bene.
Cheeso,

1
Cheeso: Il libro "Linee guida per la progettazione di strutture", nella sezione relativa alla progettazione di eccezioni personalizzate, afferma: "Fornisci (almeno) questi costruttori comuni su tutte le eccezioni". Vedi qui: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Per la correttezza della serializzazione è necessario solo il costruttore (informazioni SerializationInfo, contesto StreamingContext), il resto viene fornito per renderlo un buon punto di partenza per taglia e incolla. Quando tagli e incolli, cambierai sicuramente i nomi delle classi, quindi non penso che violare la convenzione di denominazione delle eccezioni sia significativa qui ...
Daniel Fortunov,

3
questa risposta accettata è vera anche per .NET Core? Nel core .net GetObjectDatanon viene mai invocato ... tuttavia posso ignorare ciò ToString()che viene invocato
LP13

3
Sembra che non sia così che viene fatto nel nuovo mondo. Ad esempio, letteralmente nessuna eccezione in ASP.NET Core è implementata in questo modo. Tutti omettono le cose di serializzazione: github.com/aspnet/Mvc/blob/…
bitbonk

25

L'eccezione è già serializzabile, ma è necessario sovrascrivere il GetObjectDatametodo per memorizzare le variabili e fornire un costruttore che può essere chiamato quando si reidrata l'oggetto.

Quindi il tuo esempio diventa:

[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
Spesso puoi cavartela semplicemente aggiungendo [Serializable] alla tua classe.
Hallgrim,

3
Hallgrim: L'aggiunta di [Serializable] non è sufficiente se si hanno campi aggiuntivi da serializzare.
Joe,

2
NB: "In generale questo costruttore dovrebbe essere protetto se la classe non è sigillata" - quindi il costruttore di serializzazione nel tuo esempio dovrebbe essere protetto (o, forse più appropriatamente, la classe dovrebbe essere sigillata a meno che l'ereditarietà non sia specificatamente richiesta). A parte questo, buon lavoro!
Daniel Fortunov,

Altri due errori in questo: l'attributo [serializzabile] è obbligatorio, altrimenti la serializzazione fallisce; GetObjectData deve chiamare tramite base.GetObjectData
Daniel Fortunov,

8

Implementare ISerializable e seguire lo schema normale per farlo.

È necessario taggare la classe con l'attributo [Serializable], aggiungere il supporto per quell'interfaccia e anche aggiungere il costruttore implicito (descritto in quella pagina, la ricerca implica un costruttore ). Puoi vedere un esempio della sua implementazione nel codice sotto il testo.


8

Per aggiungere le risposte corrette sopra, ho scoperto che posso evitare di fare queste cose di serializzazione personalizzate se conservo le mie proprietà personalizzate nella Dataraccolta delException classe.

Per esempio:

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

    // ...
}

Probabilmente questo è meno efficiente in termini di prestazioni rispetto alla soluzione fornita da Daniel e probabilmente funziona solo per tipi "integrali" come stringhe e numeri interi e simili.

Comunque è stato molto facile e comprensibile per me.


1
Questo è un modo semplice e piacevole per gestire ulteriori informazioni sulle eccezioni nel caso in cui sia necessario memorizzarle solo per la registrazione o qualcosa del genere. Se avessi mai avuto bisogno di accedere a questi valori aggiuntivi nel codice in un blocco catch, tuttavia, faresti affidamento sul conoscere esternamente le chiavi per i valori dei dati che non sono buoni per l'incapsulamento ecc.
Christopher King

2
Wow grazie. Continuavo a perdere in modo casuale tutte le mie variabili personalizzate aggiunte ogni volta che un'eccezione veniva riproposta usando throw;e questo risolto.
Nyerguds,

1
@ChristopherKing Perché dovresti conoscere le chiavi? Sono codificati nel getter.
Nyerguds,

1

C'era un eccellente articolo di Eric Gunnerson su MSDN "L'eccezione ben temperata", ma sembra essere stato estratto. L'URL era:

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

La risposta di Aydsman è corretta, maggiori informazioni qui:

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

Non riesco a pensare a nessun caso d'uso per un'eccezione con membri non serializzabili, ma se si evita di tentare di serializzare / deserializzare in GetObjectData e nel costruttore della deserializzazione, si dovrebbe essere OK. Contrassegnali anche con l'attributo [Non serializzato], più come documentazione che altro, poiché stai implementando tu stesso la serializzazione.


0

Segna la classe con [Serializable], anche se non sono sicuro di come un serialista gestirà bene un membro di IList.

MODIFICARE

Il post di seguito è corretto, poiché l'eccezione personalizzata ha un costruttore che accetta parametri, è necessario implementare ISerializable.

Se hai utilizzato un costruttore predefinito e hai esposto i due membri personalizzati con le proprietà getter / setter, potresti cavartela semplicemente impostando l'attributo.


-5

Devo pensare che voler serializzare un'eccezione sia una forte indicazione che stai adottando un approccio sbagliato a qualcosa. Qual è l'obiettivo finale, qui? Se si passa l'eccezione tra due processi o tra esecuzioni separate dello stesso processo, la maggior parte delle proprietà dell'eccezione non saranno comunque valide nell'altro processo.

Probabilmente avrebbe più senso estrarre le informazioni sullo stato desiderate nell'istruzione catch () e archiviarle.


9
Downvote: le linee guida di Microsoft indicano che le eccezioni dovrebbero essere serializzabili msdn.microsoft.com/en-us/library/ms229064.aspx In modo che possano essere gettate oltre un limite di dominio, ad esempio usando il telecomando.
Joe,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.