カスタム.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; }
    }
}

この例外がシリアライズおよびデシリアライズされる場合、2つのカスタムプロパティ(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ことを示す「アセンブリYを入力Xは、シリアル化可能としてマークされていません。」
  2. カスタムのシリアル化を実装する必要あります[Serializable]単独の属性では十分ではありません- Exception実装ISerializableあなたの派生クラスを意味し、カスタムのシリアル化を実装する必要があります。これには2つのステップが含まれます。
    1. シリアル化コンストラクターを提供します。このコンストラクターはprivate、クラスがのsealed場合に作成する必要がありprotectedます。
    2. GetObjectData()オーバーライドしbase.GetObjectData(info, context)、基本クラスが独自の状態を保存できるように、最後にtoを呼び出してください。

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単体テストは、上記で定義された3つの例外タイプをテストします。

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(string message)およびMyException(string message、Exception innerException)を提供することです
Joe

3
また、フレームワーク設計ガイドラインでは、例外の名前は「Exception」で終わる必要があると述べています。MyExceptionAndHereIsaQualifyingAdverbialPhraseのようなものはお勧めしません。 msdn.microsoft.com/en-us/library/ms229064.aspx かつて誰かが言ったように、ここで提供するコードはパターンとして使用されることが多いので、正しく理解するように注意する必要があります。
Cheeso、2009

1
Cheeso:カスタム例外の設計に関するセクションの「フレームワーク設計ガイドライン」には、「(少なくとも)すべての例外にこれらの共通コンストラクターを提供してください」と記載されています。ここを参照してください:blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx (SerializationInfo info、StreamingContext context)コンストラクターのみがシリアライズの正確さのために必要であり、残りはこれを良い出発点にするために提供されていますカット&ペースト。ただし、カットアンドペーストすると、クラス名は必ず変更されるため、ここでは例外の命名規則に違反することは重要ではないと思います...
Daniel Fortunov 2009

3
この受け入れられた答えは.NET Coreにも当てはまりますか?.NETコアではGetObjectData、私はオーバーライドすることができますinvoked..however取得することはありませんToString()呼び出された
LP13

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 2008

3
Hallgrim:シリアル化する追加のフィールドがある場合、[Serializable]を追加するだけでは不十分です。
Joe

2
注意:「一般に、クラスがシールされていない場合、このコンストラクタは保護する必要があります」-したがって、例のシリアル化コンストラクタは保護する必要があります(または、おそらくより適切には、継承が特に必要でない限り、クラスをシールする必要があります)。それ以外はいい仕事です!
ダニエルフォルトゥノフ2008

これに関する他の2つの誤り:[Serializable]属性は必須です。それ以外の場合、シリアル化は失敗します。GetObjectDataはbase.GetObjectDataを呼び出す必要があります
Daniel Fortunov

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

    // ...
}

おそらくこれは、Danielが提供するソリューションよりもパフォーマンスの点で効率的ではありません。、おそらく文字列や整数などの「整数」型に対してのみ機能します。

それでも、それは私にとって非常に簡単で非常に理解できました。


1
これは、ログ記録などのために格納するだけでよい場合に、追加の例外情報を処理するための優れたシンプルな方法です。あなたは今まであなたがして、カプセル化などのために良いではない外部データ値のためのキーを知ることに依存することになるがキャッチブロック内のコードでこれらの追加の値にアクセスするために必要な場合
クリストファー・キング

2
わぁ、ありがとう。使用して例外が再スローされるたびに、カスタム追加されたすべての変数をランダムに失い続けthrow;、これにより修正されました。
Nyerguds

1
@ChristopherKingなぜあなたは鍵を知る必要があるのですか?それらはゲッターにハードコードされています。
Nyerguds

1

以前はMSDNのEric Gunnersonからの優れた記事「よく調律された例外」がありましたが、それは引っ張られたようです。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プロパティを使用して2つのカスタムメンバーを公開した場合、属性を設定するだけで問題を回避できます。


-5

例外をシリアル化することは、何かに対して間違ったアプローチを取っていることを強く示していると思います。ここでの最終的な目標は何ですか?2つのプロセス間、または同じプロセスの別々の実行間で例外を渡す場合、例外のプロパティのほとんどは、とにかく他のプロセスでは有効になりません。

catch()ステートメントで必要な状態情報を抽出し、それをアーカイブする方がおそらく理にかなっています。


9
Downvote -マイクロソフトのガイドライン状態の例外は、シリアライズ可能でなければなりませんmsdn.microsoft.com/en-us/library/ms229064.aspx 彼らはRemotingを使用して例えば、AppDomainの境界を越えてスローすることができるようにします。
Joe
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.