Was ist der richtige Weg, um eine benutzerdefinierte .NET-Ausnahme serialisierbar zu machen?

224

Insbesondere, wenn die Ausnahme benutzerdefinierte Objekte enthält, die möglicherweise selbst serialisierbar sind oder nicht.

Nehmen Sie dieses Beispiel:

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

Wenn diese Ausnahme serialisiert und de-serialisiert wird, bleiben die beiden benutzerdefinierten Eigenschaften ( ResourceNameund ValidationErrors) nicht erhalten. Die Eigenschaften werden zurückgegeben null.

Gibt es ein allgemeines Codemuster für die Implementierung der Serialisierung für benutzerdefinierte Ausnahmen?

Daniel Fortunov
quelle

Antworten:

411

Basisimplementierung ohne benutzerdefinierte Eigenschaften

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

Vollständige Implementierung mit benutzerdefinierten Eigenschaften

Vollständige Implementierung einer benutzerdefinierten serialisierbaren Ausnahme ( MySerializableException) und einer abgeleiteten sealedAusnahme ( MyDerivedSerializableException).

Die wichtigsten Punkte zu dieser Implementierung sind hier zusammengefasst:

  1. Sie müssen jede abgeleitete Klasse mit dem [Serializable]Attribut dekorieren. - Dieses Attribut wird nicht von der Basisklasse geerbt. Wenn es nicht angegeben wird, schlägt die Serialisierung mit der SerializationExceptionAngabe fehl , dass "Typ X in Assembly Y nicht als serialisierbar markiert ist."
  2. Sie müssen eine benutzerdefinierte Serialisierung implementieren . Das [Serializable]Attribut allein reicht nicht aus - Exceptionimplementiert, ISerializablewas bedeutet, dass Ihre abgeleiteten Klassen auch eine benutzerdefinierte Serialisierung implementieren müssen. Dies umfasst zwei Schritte:
    1. Stellen Sie einen Serialisierungskonstruktor bereit . Dieser Konstruktor sollte sein, privatewenn Ihre Klasse ist sealed, andernfalls sollte protecteder den Zugriff auf abgeleitete Klassen ermöglichen.
    2. Überschreiben Sie GetObjectData () und stellen Sie sicher, dass Sie base.GetObjectData(info, context)am Ende bis aufrufen , damit die Basisklasse ihren eigenen Status speichert.

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

Unit Tests

MSTest-Komponententests für die drei oben definierten Ausnahmetypen.

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
quelle
3
+1: Aber wenn Sie so viel Ärger haben, würde ich den ganzen Weg gehen und alle MS-Richtlinien für die Implementierung von Ausnahmen befolgen. Eine, an die ich mich erinnern kann, ist die Bereitstellung der Standardkonstruktoren MyException (), MyException (String-Nachricht) und MyException (String-Nachricht, Exception innerException)
Joe
3
Außerdem - dass die Framework Design Guideliness besagt, dass Namen für Ausnahmen mit "Ausnahme" enden sollten . So etwas wie MyExceptionAndHereIsaQualifyingAdverbialPhrase wird nicht empfohlen. msdn.microsoft.com/en-us/library/ms229064.aspx Jemand hat einmal gesagt, der hier bereitgestellte Code wird häufig als Muster verwendet, daher sollten wir vorsichtig sein, um ihn richtig zu machen.
Cheeso
1
Cheeso: In dem Buch "Framework Design Guidelines" im Abschnitt "Entwerfen benutzerdefinierter Ausnahmen" heißt es: "Stellen Sie (zumindest) diese gemeinsamen Konstruktoren für alle Ausnahmen bereit." Siehe hier: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Für die Serialisierungskorrektheit wird nur der Konstruktor (SerializationInfo info, StreamingContext context) benötigt, der Rest wird bereitgestellt, um dies zu einem guten Ausgangspunkt zu machen Ausschneiden und Einfügen. Wenn Sie jedoch ausschneiden und einfügen, werden Sie sicherlich die Klassennamen ändern, daher denke ich nicht, dass ein Verstoß gegen die Ausnahmeregelungskonvention hier von Bedeutung ist ...
Daniel Fortunov
3
Trifft diese akzeptierte Antwort auch für .NET Core zu? In .net wird der Kern GetObjectDatanie aufgerufen. Ich kann jedoch überschreiben, ToString()was
aufgerufen
3
Es scheint, dass dies nicht so ist, wie es in der neuen Welt gemacht wird. Beispielsweise wird auf diese Weise buchstäblich keine Ausnahme in ASP.NET Core implementiert. Sie alle lassen das Serialisierungsmaterial
weg
25

Die Ausnahme ist bereits serialisierbar, aber Sie müssen die GetObjectDataMethode zum Speichern Ihrer Variablen überschreiben und einen Konstruktor bereitstellen, der beim erneuten Hydratisieren Ihres Objekts aufgerufen werden kann.

So wird Ihr Beispiel:

[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
quelle
1
Oft können Sie davonkommen, indem Sie Ihrer Klasse nur [Serializable] hinzufügen.
Hallgrim
3
Hallgrim: Das Hinzufügen von [Serializable] reicht nicht aus, wenn Sie zusätzliche Felder zum Serialisieren haben.
Joe
2
NB: "Im Allgemeinen sollte dieser Konstruktor geschützt werden, wenn die Klasse nicht versiegelt ist" - daher sollte der Serialisierungskonstruktor in Ihrem Beispiel geschützt werden (oder besser gesagt, die Klasse sollte versiegelt werden, sofern keine Vererbung ausdrücklich erforderlich ist). Ansonsten gute Arbeit!
Daniel Fortunov
Zwei weitere Fehler: Das Attribut [Serializable] ist obligatorisch, andernfalls schlägt die Serialisierung fehl. GetObjectData muss zu base.GetObjectData
Daniel Fortunov
8

Implementieren Sie ISerializable und folgen Sie dazu dem normalen Muster .

Sie müssen die Klasse mit dem Attribut [Serializable] kennzeichnen, Unterstützung für diese Schnittstelle hinzufügen und den impliziten Konstruktor hinzufügen (auf dieser Seite beschrieben, Suche nach impliziert einen Konstruktor ). Ein Beispiel für die Implementierung finden Sie im Code unter dem Text.

Lasse V. Karlsen
quelle
8

Um die obigen richtigen Antworten zu ergänzen, habe ich festgestellt, dass ich diese benutzerdefinierten Serialisierungsaufgaben vermeiden kann, wenn ich meine benutzerdefinierten Eigenschaften in der DataAuflistung der ExceptionKlasse speichere .

Z.B:

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

    // ...
}

Wahrscheinlich ist dies in Bezug auf die Leistung weniger effizient als die von Daniel bereitgestellte Lösung und funktioniert wahrscheinlich nur für "integrale" Typen wie Zeichenfolgen und Ganzzahlen und dergleichen.

Trotzdem war es für mich sehr einfach und sehr verständlich.

Uwe Keim
quelle
1
Dies ist eine nette und einfache Möglichkeit, zusätzliche Ausnahmeinformationen zu verarbeiten, wenn Sie sie nur für die Protokollierung oder ähnliches speichern müssen. Wenn Sie jemals auf diese zusätzlichen Werte im Code in einem Catch-Block zugreifen müssten, würden Sie sich darauf verlassen, die Schlüssel für die Datenwerte extern zu kennen, was für die Kapselung usw. nicht gut ist.
Christopher King
2
Wow Danke. Ich habe zufällig alle meine benutzerdefinierten hinzugefügten Variablen verloren, wenn eine Ausnahme mit erneut ausgelöst wurde, throw;und dies hat sie behoben.
Nyerguds
1
@ChristopherKing Warum sollten Sie die Schlüssel kennen müssen? Sie sind im Getter fest codiert.
Nyerguds
1

Früher gab es einen ausgezeichneten Artikel von Eric Gunnerson über MSDN "Die wohltemperierte Ausnahme", aber er scheint gezogen worden zu sein. Die URL war:

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

Aydsmans Antwort ist richtig, mehr Infos hier:

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

Ich kann mir keinen Anwendungsfall für eine Ausnahme mit nicht serialisierbaren Mitgliedern vorstellen, aber wenn Sie vermeiden, sie in GetObjectData und im Deserialisierungskonstruktor zu serialisieren / deserialisieren, sollten Sie in Ordnung sein. Markieren Sie sie auch mit dem Attribut [NonSerialized], mehr als alles andere als Dokumentation, da Sie die Serialisierung selbst implementieren.

Joe
quelle
0

Markieren Sie die Klasse mit [Serializable], obwohl ich nicht sicher bin, wie gut ein IList-Mitglied vom Serializer behandelt wird.

BEARBEITEN

Der folgende Beitrag ist korrekt, da Ihre benutzerdefinierte Ausnahme einen Konstruktor enthält, der Parameter akzeptiert, müssen Sie ISerializable implementieren.

Wenn Sie einen Standardkonstruktor verwendet und die beiden benutzerdefinierten Elemente mit Getter / Setter-Eigenschaften verfügbar gemacht haben, können Sie das Attribut nur festlegen.

David Hill
quelle
-5

Ich muss denken, dass der Wunsch, eine Ausnahme zu serialisieren, ein starkes Indiz dafür ist, dass Sie bei etwas falsch vorgehen. Was ist hier das ultimative Ziel? Wenn Sie die Ausnahme zwischen zwei Prozessen oder zwischen separaten Läufen desselben Prozesses übergeben, sind die meisten Eigenschaften der Ausnahme im anderen Prozess ohnehin nicht gültig.

Es wäre wahrscheinlich sinnvoller, die gewünschten Statusinformationen in der Anweisung catch () zu extrahieren und diese zu archivieren.

Mark Bessey
quelle
9
Downvote - Die Ausnahmen der Microsoft-Richtlinien sollten serialisierbar sein. Msdn.microsoft.com/en-us/library/ms229064.aspx Sie können also über eine Appdomain-Grenze geworfen werden, z. B. mithilfe von Remoting.
Joe