Каков правильный способ сделать пользовательское исключение.СЕТЕВОЕ исключение сериализуемым?
-
01-07-2019 - |
Вопрос
Более конкретно, когда исключение содержит пользовательские объекты, которые сами по себе могут быть сериализуемыми, а могут и не быть.
Возьмем этот пример:
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
.
Существует ли общий шаблон кода для реализации сериализации для пользовательского исключения?
Решение
Базовая реализация, без пользовательских свойств
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
).
Основные моменты, касающиеся этой реализации, кратко изложены здесь:
- Ты необходимо украсить каждый производный класс
[Serializable]
атрибут — Этот атрибут не наследуется от базового класса, и если он не указан, сериализация завершится ошибкой сSerializationException
заявляя , что "Тип X в сборке Y не помечен как сериализуемый". - Ты необходимо реализовать пользовательскую сериализацию.Тот Самый
[Serializable]
одного атрибута недостаточно —Exception
орудия трудаISerializable
это означает, что ваши производные классы также должны реализовывать пользовательскую сериализацию.Это включает в себя два этапа:- Предоставьте конструктор сериализации.Этот конструктор должен быть
private
если ваш класс являетсяsealed
, в противном случае это должно бытьprotected
чтобы разрешить доступ к производным классам. - Переопределить 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);
}
}
}
Производное serializableexceptionwithadditionalcustomproperties.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()");
}
}
}
Другие советы
Исключение уже сериализуемо, но вам нужно переопределить 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>));
}
}
Реализуйте ISerializable и следуйте инструкциям нормальная картина за то, что сделал это.
Вам нужно пометить класс атрибутом [Serializable] и добавить поддержку для этого интерфейса, а также добавить подразумеваемый конструктор (описанный на этой странице, выполните поиск подразумевает конструктор).Вы можете увидеть пример его реализации в коде под текстом.
Чтобы добавить к правильным ответам выше, я обнаружил, что могу избежать выполнения этой пользовательской сериализации, если сохраню свои пользовательские свойства в 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; }
}
// ...
}
Вероятно, это менее эффективно с точки зрения производительности, чем решение, предоставленное Дэниелом и, вероятно, работает только для "целых" типов, таких как строки, целые числа и тому подобное.
Тем не менее, для меня это было очень легко и очень понятно.
Раньше на 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 и конструкторе десериализации, все должно быть в порядке.Также пометьте их атрибутом [Несериализованный], скорее как документацию, чем что-либо еще, поскольку вы реализуете сериализацию самостоятельно.
Пометьте класс с помощью [Serializable] , хотя я не уверен, насколько хорошо элемент IList будет обрабатываться сериализатором.
Редактировать
Сообщение ниже является правильным, поскольку в вашем пользовательском исключении есть конструктор, который принимает параметры, вы должны реализовать ISerializable.
Если бы вы использовали конструктор по умолчанию и предоставили два пользовательских элемента со свойствами getter / setter, вам могло бы сойти с рук простое задание атрибута.
Я должен думать, что желание сериализовать исключение является убедительным признаком того, что вы применяете неправильный подход к чему-то.Какова здесь конечная цель?Если вы передаете исключение между двумя процессами или между отдельными запусками одного и того же процесса, то большинство свойств исключения в любом случае не будут действительны в другом процессе.
Вероятно, было бы разумнее извлечь нужную информацию о состоянии из инструкции catch() и заархивировать ее.