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

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

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

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.

Есть ли общий шаблон кода для реализации сериализации для настраиваемого исключения?


person Daniel Fortunov    schedule 18.09.2008    source источник


Ответы (8)


Базовая реализация без настраиваемых свойств

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. You must implement custom serialization. The [Serializable] attribute alone is not enough — Exception implements ISerializable which means your derived classes must also implement custom serialization. This involves two steps:
    1. Provide a serialization constructor. This constructor should be private if your class is sealed, otherwise it should be protected to allow access to derived classes.
    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()");
        }
    }
}
person Daniel Fortunov    schedule 19.09.2008
comment
+1: но если у вас возникнут такие большие проблемы, я бы пошел полностью и следовал всем рекомендациям MS по реализации исключений. Я могу вспомнить, что нужно предоставить стандартные конструкторы MyException (), MyException (строковое сообщение) и MyException (строковое сообщение, Exception innerException). - person Joe; 19.09.2008
comment
Добавлены стандартные конструкторы исключений. Я также добавил условие защиты, чтобы генерировать исключение ArgumentNullException, если информационный параметр GetObjectData имеет значение null. - person Daniel Fortunov; 19.09.2008
comment
Где правила, согласно которым я должен реализовать стандартные конструкторы? Связаны ли эти ctors с правильностью сериализуемости? Другими словами, для сериализуемого, должен ли я реализовать ctor по умолчанию? Встроенный ctor исключения? - person Cheeso; 01.07.2009
comment
Кроме того, в Руководстве по проектированию фреймворка сказано, что имена исключений должны заканчиваться на Exception. Что-то вроде MyExceptionAndHereIsaQualifyingAdverbialPhrase не рекомендуется. msdn.microsoft.com/en-us/library/ms229064.aspx Кто-то однажды сказал, что код, который мы здесь приводим, часто используется в качестве шаблона, поэтому мы должны быть осторожны, чтобы сделать его правильным. - person Cheeso; 01.07.2009
comment
Cheeso: В книге Framework Design Guidelines в разделе «Разработка настраиваемых исключений» говорится: Предоставляйте (по крайней мере) эти общие конструкторы для всех исключений. См. Здесь: blogs.msdn.com/kcwalina/archive/2006 /07/05/657268.aspx Для корректной сериализации нужен только конструктор (SerializationInfo info, StreamingContext context), остальное предоставлено, чтобы сделать его хорошей отправной точкой для вырезания и вставки. Однако, когда вы копируете и вставляете, вы обязательно измените имена классов, поэтому я не думаю, что нарушение соглашения об именах исключений здесь важно ... - person Daniel Fortunov; 02.07.2009
comment
Этот ответ (хотя и отличный) не учитывает версии. Взгляните на stackoverflow.com/questions/2613874/ . - person Jared Moore; 28.09.2011
comment
Я не понимаю разницы между настраиваемыми свойствами и дополнительными настраиваемыми свойствами? Почему первое не запечатано, а второе запечатано? А также, почему два примера с большим количеством свойств? - person Didier A.; 09.05.2013
comment
Для части [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] см. Безопасность и сериализацию в MSDN. - person Palec; 18.03.2017
comment
Верен ли этот принятый ответ и для .NET Core? В ядре .net GetObjectData никогда не вызывается .. однако я могу переопределить ToString(), который вызывается - person LP13; 02.03.2018
comment
Похоже, что в новом мире это не так. Например, в ASP.NET Core таким образом практически не реализовано никаких исключений. Все они опускают материал о сериализации: github.com/aspnet/Mvc/blob/ - person bitbonk; 12.03.2018
comment
System.Net.Sockets.SocketException и System.Net.Http.HttpRequestException НЕ СЕРИАЛИЗИРУЕМЫЙ? - person Kiquenet; 13.03.2018
comment
Обновление для предыдущих комментариев - .NET Core поддерживает двоичную сериализацию с подмножеством типов, включая Exception и несколько важных типов, производных от Exception, которые можно найти здесь. Причину изменений можно найти здесь: Снижение масштабирования Serializable для .NET Core 2.0 - person csrowell; 18.10.2018
comment
Возможно, в наши дни не стоит использовать SecurityPermissionAttribute ... docs .microsoft.com / en-us / dotnet / framework / misc / - person bytedev; 21.01.2021

Исключение уже сериализуемо, но вам нужно переопределить метод 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>));
    }

}
person Adrian Clark    schedule 18.09.2008
comment
Часто можно просто добавить в класс [Serializable]. - person Hallgrim; 18.09.2008
comment
Халлгрим: добавления [Serializable] недостаточно, если у вас есть дополнительные поля для сериализации. - person Joe; 18.09.2008
comment
NB: В общем, этот конструктор должен быть защищен, если класс не запечатан - поэтому конструктор сериализации в вашем примере должен быть защищен (или, что более уместно, класс должен быть запечатан, если наследование не требуется специально). Кроме этого, хорошая работа! - person Daniel Fortunov; 19.09.2008
comment
Две другие ошибки в этом: атрибут [Serializable] является обязательным, иначе сериализация не удастся; GetObjectData должен вызывать базу. - person Daniel Fortunov; 19.09.2008

Чтобы добавить к правильным ответам, приведенным выше, я обнаружил, что могу избежать этой настраиваемой сериализации, если сохраню свои настраиваемые свойства в _ 1_ коллекция Exception класса.

E.g.:

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

    // ...
}

Вероятно, это менее эффективно с точки зрения производительности, чем решение, предоставленное Дэниелом, и, вероятно, работает только для «интегральных» типов, таких как строки. и целые числа и тому подобное.

И все же для меня это было очень легко и очень понятно.

person Uwe Keim    schedule 12.11.2014
comment
Это хороший и простой способ обрабатывать дополнительную информацию об исключении в случае, когда вам нужно только сохранить ее для регистрации или чего-то в этом роде. Если вам когда-либо понадобился доступ к этим дополнительным значениям в коде в блоке catch, вы должны были бы полагаться на знание ключей для значений данных извне, что не подходит для инкапсуляции и т. Д. - person Christopher King; 06.03.2015
comment
Вау, спасибо. Я продолжал случайным образом терять все мои пользовательские добавленные переменные всякий раз, когда исключение повторно генерировалось с использованием throw;, и это исправляло его. - person Nyerguds; 26.02.2016
comment
@ChristopherKing Зачем вам нужны ключи? Они жестко запрограммированы в геттере. - person Nyerguds; 26.02.2016

Реализуйте ISerializable и следуйте нормальному шаблон для этого.

Вам нужно пометить класс атрибутом [Serializable] и добавить поддержку этого интерфейса, а также добавить подразумеваемый конструктор (описанный на этой странице, поиск подразумевает конструктор). Вы можете увидеть пример его реализации в коде под текстом.

person Lasse V. Karlsen    schedule 18.09.2008

Раньше на 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

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

person Joe    schedule 18.09.2008

Отметьте класс с помощью [Serializable], хотя я не уверен, насколько хорошо член IList будет обрабатываться сериализатором.

ИЗМЕНИТЬ

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

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

person David Hill    schedule 18.09.2008

В .NET Core .Net 5.0 и выше не используют Serializable, поскольку Microsoft следует методам защиты от угроз, изложенным в BinaryFormatter .

Используйте пример хранения в Data Collection

person user2205317    schedule 19.03.2021
comment
Нет, это неправильно. Согласно документации Microsoft: - msdn.microsoft.com/en-us/library /ms229064.aspx - person ILoveLogCat; 20.03.2021
comment
@Xenikh - вы ссылаетесь на древнюю документацию (2013 г.). @ user2205317 - можете ли вы указать на какие-либо официальные документы, в которых говорится о том, что шаблон сериализации исключений устарел? Код ASP.NET 5.0 включал исключения, которые включают и не включают Serializable, например: github.com/dotnet/aspnetcore/blob/v5.0.4/src/Http/Routing/src/ github.com/dotnet/aspnetcore/blob/v5.0.4/src/Mvc / - person crimbo; 26.03.2021
comment
Если Microsoft уведомляет о наличии проблемы безопасности (угрозы внедрения) в BinaryFormatter, SoapFormatter, LosFormatter, NetDataContractSerializer, ObjectStateFormatter, который использует Serializable, в структуре BCL не осталось средств форматирования, поддерживающих этот шаблон. Ищу повод его поддержать. Рекомендуется использовать и альтернативу, например XML, JSON, YMAL ... - person user2205317; 14.04.2021

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

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

person Mark Bessey    schedule 18.09.2008
comment
Голосование против - исключения, указанные в рекомендациях Microsoft, должны быть сериализуемыми msdn.microsoft.com/en- us / library / ms229064.aspx Чтобы их можно было перебросить через границу домена приложения, например с помощью удаленного взаимодействия. - person Joe; 18.09.2008