Jaki jest właściwy sposób, aby niestandardowy wyjątek .NET mógł zostać przekształcony do postaci szeregowej?

224

Mówiąc dokładniej, gdy wyjątek zawiera obiekty niestandardowe, które mogą, ale nie muszą być serializowane.

Weź ten przykład:

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

Jeśli ten wyjątek zostanie zserializowany i zserializowany, dwie niestandardowe właściwości ( ResourceNamei ValidationErrors) nie zostaną zachowane. Właściwości zostaną zwrócone null.

Czy istnieje wspólny wzorzec kodu do implementacji serializacji dla wyjątku niestandardowego?

Daniel Fortunov
źródło

Odpowiedzi:

411

Implementacja podstawowa, bez właściwości niestandardowych

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

Pełna implementacja z niestandardowymi właściwościami

Pełna implementacja niestandardowego wyjątku szeregowalnego ( MySerializableException) i sealedwyjątku pochodnego ( MyDerivedSerializableException).

Główne punkty dotyczące tego wdrożenia zostały podsumowane tutaj:

  1. Państwo musi ozdobić każdą klasę pochodzącą z [Serializable]atrybutem - ten atrybut nie jest dziedziczona z klasy bazowej, a jeśli nie jest określona, serializacji nie powiedzie się z SerializationExceptionstwierdzając, że „X Type w Assembly Y nie jest oznaczony jako możliwy do serializacji.”
  2. Musisz zaimplementować niestandardową serializację . Sam [Serializable]atrybut nie wystarczy - Exceptionimplementuje, ISerializableco oznacza, że ​​klasy pochodne muszą również implementować niestandardową serializację. Obejmuje to dwa kroki:
    1. Podaj konstruktor serializacji . Ten konstruktor powinien być, privatejeśli twoja klasa jest sealed, w przeciwnym razie powinien protectedumożliwić dostęp do klas pochodnych.
    2. Zastąp GetObjectData () i upewnij się, że wywołujesz base.GetObjectData(info, context)na końcu, aby klasa podstawowa mogła zapisać swój własny stan.

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

Testy jednostkowe

Testy jednostkowe MSTest dla trzech typów wyjątków określonych powyżej.

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
źródło
3
+1: ale jeśli będziesz miał tyle kłopotów, pójdę na całość i zastosuję się do wszystkich wytycznych MS dotyczących wdrażania wyjątków. Jedyne, co pamiętam, to zapewnienie standardowych konstruktorów MyException (), MyException (komunikat tekstowy) i MyException (komunikat tekstowy, wyjątek innerException)
Joe
3
Ponadto - że wytyczne w sprawie projektu ramowego mówią, że nazwy wyjątków powinny kończyć się na „wyjątku”. Coś takiego jak MyExceptionAndHereIsaQualifyingAdverbialPhrase jest niezalecane. msdn.microsoft.com/en-us/library/ms229064.aspx Ktoś kiedyś powiedział, że kod, który tu podajemy, jest często używany jako wzorzec, więc powinniśmy uważać, aby go poprawnie.
Cheeso,
1
Cheeso: Książka „Wytyczne dotyczące projektowania ram”, w części dotyczącej projektowania wyjątków niestandardowych, stwierdza: „Podaj (przynajmniej) te wspólne konstruktory dla wszystkich wyjątków”. Zobacz tutaj: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Do poprawności serializacji potrzebny jest tylko konstruktor (informacje SerializationInfo, kontekst StreamingContext). Reszta jest zapewniona, aby był to dobry punkt wyjścia dla wytnij i wklej. Kiedy jednak wycinasz i wklejasz, na pewno zmienisz nazwy klas, dlatego nie sądzę, że naruszenie konwencji nazewnictwa wyjątków jest tutaj istotne ...
Daniel Fortunov,
3
czy ta zaakceptowana odpowiedź jest prawdziwa również w przypadku .NET Core? W .net core GetObjectDatanigdy się nie wywołuje .. jednak mogę nadpisać to, ToString()co zostanie
wywołane
3
Wygląda na to, że nie w ten sposób dzieje się to w nowym świecie. Na przykład dosłownie żaden wyjątek w programie ASP.NET Core nie jest implementowany w ten sposób. Wszyscy pomijają serializację: github.com/aspnet/Mvc/blob/…
bitbonk
25

Wyjątek jest już możliwy do serializacji, ale należy przesłonić GetObjectDatametodę przechowywania zmiennych i zapewnić konstruktor, który można wywołać podczas ponownego uwodnienia obiektu.

Twój przykład staje się:

[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
źródło
1
Często możesz uniknąć dodania [Serializable] do swojej klasy.
Hallgrim,
3
Hallgrim: Dodanie [Serializable] nie wystarczy, jeśli masz dodatkowe pola do serializacji.
Joe
2
Uwaga: „Ogólnie rzecz biorąc, ten konstruktor powinien być chroniony, jeśli klasa nie jest zapieczętowana” - więc konstruktor serializacji w twoim przykładzie powinien być chroniony (lub, co bardziej odpowiednie, klasa powinna być zapieczętowana, chyba że dziedziczenie jest wyraźnie wymagane). Poza tym dobra robota!
Daniel Fortunov,
Dwa inne błędy w tym: atrybut [Serializable] jest obowiązkowy, w przeciwnym razie serializacja nie powiedzie się; GetObjectData musi zadzwonić do base.GetObjectData
Daniel Fortunov
8

Zaimplementuj ISerializable i postępuj zgodnie z normalnym wzorcem .

Musisz otagować klasę za pomocą atrybutu [Serializable] i dodać obsługę tego interfejsu, a także dodać domyślny konstruktor (opis na tej stronie, wyszukiwanie sugeruje konstruktor ). Przykład jego implementacji można zobaczyć w kodzie pod tekstem.

Lasse V. Karlsen
źródło
8

Aby dodać do poprawnych odpowiedzi powyżej, odkryłem, że mogę uniknąć robi ten niestandardowy serializacji rzeczy jeśli przechowywać swoje właściwości niestandardowe w Datakolekcji w Exceptionklasie.

Na przykład:

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

    // ...
}

Prawdopodobnie jest to mniej wydajne pod względem wydajności niż rozwiązanie dostarczone przez Daniela i prawdopodobnie działa tylko dla typów „integralnych”, takich jak łańcuchy i liczby całkowite i tym podobne.

Mimo wszystko było to dla mnie bardzo łatwe i zrozumiałe.

Uwe Keim
źródło
1
Jest to przyjemny i prosty sposób obsługi dodatkowych informacji o wyjątkach w przypadku, gdy wystarczy je zapisać do logowania lub coś w tym rodzaju. Gdybyś kiedykolwiek potrzebował uzyskać dostęp do tych dodatkowych wartości w kodzie w catch-block, musiałbyś jednak polegać na znajomości kluczy wartości danych na zewnątrz, co nie jest dobre do enkapsulacji itp.
Christopher King
2
Wow, dziękuję Losowo traciłem wszystkie niestandardowe zmienne dodane za każdym razem, gdy wyjątek był ponownie zgłaszany za pomocą throw;i to naprawiało.
Nyerguds
1
@ChristopherKing Dlaczego chcesz znać klucze? Są zakodowane na stałe w getterze.
Nyerguds
1

Był świetny artykuł Erica Gunnersona na MSDN „Wyjątkowy temperament”, ale wydaje się, że został wyciągnięty. Adres URL to:

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

Odpowiedź Aydsmana jest poprawna, więcej informacji tutaj:

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

Nie mogę wymyślić żadnego przypadku użycia wyjątku z członami, których nie można serializować, ale jeśli unikniesz próby serializacji / deserializacji ich w GetObjectData i konstruktorze deserializacji, powinieneś być w porządku. Oznacz je również atrybutem [NonSerialized], bardziej jako dokumentację niż cokolwiek innego, ponieważ serializację wdrażasz samodzielnie.

Joe
źródło
0

Oznacz klasę jako [Serializable], chociaż nie jestem pewien, jak dobrze członek IList będzie obsługiwany przez serializator.

EDYTOWAĆ

Poniższy post jest poprawny, ponieważ Twój wyjątek niestandardowy ma konstruktor, który pobiera parametry, musisz zaimplementować ISerializable.

Jeśli użyłeś domyślnego konstruktora i odsłoniłeś dwa niestandardowe elementy z właściwościami getter / setter, możesz odejść od samego ustawienia atrybutu.

David Hill
źródło
-5

Muszę pomyśleć, że chęć serializacji wyjątku jest silnym sygnałem, że źle do czegoś podchodzisz. Jaki jest tutaj ostateczny cel? Jeśli przekazujesz wyjątek między dwoma procesami lub między osobnymi uruchomieniami tego samego procesu, większość właściwości wyjątku i tak nie będzie poprawna w drugim procesie.

Prawdopodobnie bardziej sensowne byłoby wyodrębnienie wymaganych informacji o stanie w instrukcji catch () i zarchiwizowanie ich.

Mark Bessey
źródło
9
Downvote - wyjątki stanu z wytycznych Microsoft powinny być możliwe do serializacji msdn.microsoft.com/en-us/library/ms229064.aspx, aby mogły zostać wyrzucone poza granicę domeny, np. Za pomocą usługi zdalnej.
Joe