Как правильно сделать сериализуемую пользовательскую исключительную ситуацию .NET исключительной?

225

Более конкретно, когда исключение содержит пользовательские объекты, которые сами могут или не могут быть сериализуемыми.

Возьмите этот пример:

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 (строковое сообщение, Exception innerException)
Джо
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] в свой класс.
Халлгрим
3
Халлгрим: Добавление [Сериализуемый] недостаточно, если у вас есть дополнительные поля для сериализации.
Джо
2
NB: «В общем случае этот конструктор должен быть защищен, если класс не запечатан» - поэтому конструктор сериализации в вашем примере должен быть защищен (или, возможно, более уместно, класс должен быть запечатан, если наследование не требуется специально). Кроме этого, хорошая работа!
Даниэль Фортунов
Две другие ошибки в этом: атрибут [Сериализуемый] является обязательным, в противном случае сериализация не выполняется; GetObjectData должен вызывать до base.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 будет обрабатываться сериализатором.

РЕДАКТИРОВАТЬ

Сообщение ниже является правильным, потому что ваше пользовательское исключение имеет конструктор, который принимает параметры, вы должны реализовать ISerializable.

Если вы использовали конструктор по умолчанию и предоставили двум пользовательским элементам свойства getter / setter, вы можете просто установить атрибут.

Дэвид Хилл
источник
-5

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

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

Марк Бесси
источник
9
Downvote - исключения состояния руководящих принципов Microsoft должны быть сериализуемыми msdn.microsoft.com/en-us/library/ms229064.aspx, чтобы их можно было перекинуть через границу домена приложения, например, с помощью удаленного взаимодействия.
Джо