Який правильний спосіб зробити серійний доступ до користувальницької .NET Exception?


224

Більш конкретно, коли виняток містить власні об'єкти, які можуть або не можуть бути самими серіалізаційними.

Візьмемо цей приклад:

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

Якщо цей Виняток буде серіалізовано та десеріалізовано, два користувацькі властивості ( ResourceNameта ValidationErrors) не будуть збережені. Властивості повернуться null.

Чи існує загальна модель коду для здійснення серіалізації для спеціального винятку?

Відповіді:


411

Базова реалізація, без спеціальних властивостей

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

Повна реалізація з користувацькими властивостями

Повна реалізація користувацького серіалізаційного винятку ( MySerializableException) та похідного sealedвинятку ( MyDerivedSerializableException).

Основні моменти щодо цієї реалізації зведені тут:

  1. Ви повинні прикрасити кожен похідний клас [Serializable]атрибутом - Цей атрибут не успадковується від базового класу, і якщо він не вказаний, серіалізація буде невдалою, SerializationExceptionзаявивши, що "Тип X у Асамблеї Y не позначений як серіалізаційний".
  2. Ви повинні реалізувати власну серіалізацію . [Serializable]У поодинці атрибут мало - ExceptionзнаряддяISerializable , які означають , що ваші похідні класи повинні реалізувати власні сериализации. Це включає два кроки:
    1. Надайте конструктор серіалізації . Цей конструктор повинен бути, privateякщо ваш клас є sealed, інакше він повинен бутиprotected дозволити доступ до похідних класів.
    2. Замініть GetObjectData () і переконайтеся, що ви зателефонували до base.GetObjectData(info, context)кінця, щоб базовий клас врятував власний стан.

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

Тестові одиниці

Тест одиниць MSTest для трьох типів винятків, визначених вище.

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: але якщо ви зіткнетеся з такою проблемою, я б пройшов увесь шлях і дотримувався всіх рекомендацій MS щодо впровадження винятків. Я можу згадати - надати стандартні конструктори MyException (), MyException (повідомлення рядка) та MyException (повідомлення рядка, виключення InternalException)
Джо,

3
Також - що Рамкове керівництво проектування говорить, що назви винятків повинні закінчуватися на "Виняток". Щось на зразок MyExceptionAndHereIsaQualifyingAdverbialPhrase не рекомендується. msdn.microsoft.com/en-us/library/ms229064.aspx Хтось одного разу сказав, код, який ми надаємо тут, часто використовується як зразок, тому ми повинні бути обережними, щоб виправити це.
Cheeso

1
Cheeso: У книзі "Рамкові рекомендації щодо дизайну" в розділі "Проектування спеціальних винятків" зазначено: "Надайте (принаймні) ці поширені конструктори за всіма винятками". Дивіться тут: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Для правильності серіалізації потрібен лише конструктор (інформація про SerializationInfo, контекст StreamingContext), решта надається для того, щоб зробити це гарною відправною точкою для вирізати і вставити. Якщо ви вирізаєте та вставте, ви, безумовно, зміните назви класів, тому я не думаю, що тут порушення принципу іменування виключень є важливим ...
Даніель Фортунов,

3
чи відповідає ця прийнята відповідь і для .NET Core? В ядро ​​.net GetObjectDataніколи не викликається. Проте я можу перекрити те, ToString()що отримує
запрошення

3
Здається, що це не так, як це робиться в новому світі. Наприклад, буквально не виняток в ASP.NET Core реалізований таким чином. Усі вони опускають матеріали про серіалізацію: github.com/aspnet/Mvc/blob/…
bitbonk

25

Виняток уже є серіалізаційним, але вам потрібно замінити GetObjectDataметод для зберігання змінних та надання конструктора, який можна викликати при повторній гідратації об'єкта.

Тож ваш приклад стає:

[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
Часто ви можете піти, просто додавши [Serializable] до свого класу.
Hallgrim

3
Hallgrim: Додавання [Serializable] недостатньо, якщо у вас є додаткові поля для серіалізації.
Джо

2
Примітка: "Загалом цей конструктор повинен бути захищений, якщо клас не запечатаний" - тому конструктор серіалізації у вашому прикладі повинен бути захищений (або, можливо, більш доцільно, клас повинен бути запечатаний, якщо спадкування конкретно не вимагається). Крім цього, хороша робота!
Даніель Фортунов

Ще дві помилки в цьому: атрибут [Serializable] є обов'язковим, інакше серіалізація не вдається; GetObjectData повинен зателефонувати на базу.GetObjectData
Даніель

8

Реалізуйте ISerializable та дотримуйтесь звичайну схему для цього.

Вам потрібно позначити клас атрибутом [Serializable] та додати підтримку цього інтерфейсу, а також додати конструктор, що мається на увазі (описано на цій сторінці; пошук має на увазі конструктор ). Приклад його реалізації ви можете побачити в коді під текстом.


8

Щоб додати правильні відповіді вище, я виявив , що я можу не робити цього призначену для користувача сериализацию речі , якщо я зберігаю призначені для користувача властивості в Dataколекції цього Exceptionкласу.

Наприклад:

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

    // ...
}

Ймовірно, це менш ефективно з точки зору продуктивності, ніж рішення, яке надає Даніель і, ймовірно, працює лише для "інтегральних" типів, таких як рядки та цілі числа тощо.

І все-таки це було дуже легко і дуже зрозуміло для мене.


1
Це приємний і простий спосіб обробляти додаткову інформацію про винятки в тому випадку, коли вам потрібно зберігати її лише для ведення журналів або чогось подібного. Якщо вам коли-небудь знадобився доступ до цих додаткових значень у коді в блоці вилову, однак ви покладаєтесь на знання ключів для значень даних зовні, що не годиться для інкапсуляції тощо.
Крістофер Кінг

2
Нічого, дякую. Я випадково втрачав усі власні додані змінні, коли виняток було перезаписано за допомогою, throw;і це виправлено.
Nyerguds

1
@ChristopherKing Навіщо вам потрібно знати ключі? Вони жорстко закодовані в геттер.
Nyerguds

1

Раніше була чудова стаття Еріка Ганнерсона про MSDN "Добре загартований виняток", але, схоже, її витягнули. URL-адреса була такою:

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

Відповідь Айдсмана правильна, більше інформації тут:

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

Я не можу придумати жодного випадку використання для Винятку з членами, які не серіалізуються, але якщо ви не намагаєтеся їх серіалізувати / дезаріалізувати в GetObjectData та конструкторі деріаріалізації, ви повинні бути в порядку. Також позначте їх атрибутом [NonSerialized], більше як документацією, ніж будь-що інше, оскільки ви самі здійснюєте серіалізацію.


0

Позначте клас із [Serializable], хоча я не впевнений, наскільки добре член IList буде обробляти серіалізатор.

EDIT

Повідомлення нижче правильне, тому що у вашому користувальницькому винятку є конструктор, який приймає параметри, ви повинні реалізувати ISerializable.

Якщо ви використовували конструктор за замовчуванням і оголювали два користувацькі члени з властивостями getter / setter, ви можете піти, просто встановивши атрибут.


-5

Я маю подумати, що прагнення серіалізувати виняток є яскравим свідченням того, що ви неправильно підходите до чогось. Яка тут кінцева мета? Якщо ви передаєте виняток між двома процесами або між окремими запусками одного і того ж процесу, більшість властивостей цього винятку в будь-якому випадку не дійсні.

Можливо, було б більше сенсу витягти інформацію про стан, яку ви хочете, у заяві catch (), і заархівувати її.


9
Downvote - винятки з положень керівництва Microsoft повинні бути серіалізаційними msdn.microsoft.com/en-us/library/ms229064.aspx, щоб їх можна було перенести через межу додатка, наприклад, за допомогою видалення.
Джо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.