Apa cara yang benar untuk membuat .NET Exception serializable khusus?

225

Lebih khusus lagi, ketika pengecualian berisi objek kustom yang mungkin atau mungkin tidak dapat serial.

Ambil contoh ini:

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

Jika Pengecualian ini serial dan de-serial, dua properti khusus ( ResourceNamedan ValidationErrors) tidak akan dipertahankan. Properti akan kembali null.

Apakah ada pola kode umum untuk menerapkan serialisasi untuk pengecualian khusus?

Daniel Fortunov
sumber

Jawaban:

411

Implementasi basis, tanpa properti khusus

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

Implementasi penuh, dengan properti khusus

Implementasi lengkap dari pengecualian serializable khusus ( MySerializableException), dan sealedpengecualian yang diturunkan ( MyDerivedSerializableException).

Poin utama tentang implementasi ini dirangkum di sini:

  1. Anda harus menghiasi setiap kelas turunan dengan [Serializable]atribut - Atribut ini tidak diwarisi dari kelas dasar, dan jika tidak ditentukan, serialisasi akan gagal dengan SerializationExceptionmenyatakan bahwa "Tipe X di Majelis Y tidak ditandai sebagai serializable."
  2. Anda harus menerapkan serialisasi khusus . The [Serializable]atribut saja tidak cukup - Exceptionalat ISerializableyang berarti kelas turunan juga harus menerapkan serialisasi kustom. Ini melibatkan dua langkah:
    1. Berikan konstruktor serialisasi . Konstruktor ini harus privatejika kelas Anda sealed, jika tidak, harus protectedmemungkinkan akses ke kelas turunan.
    2. Override GetObjectData () dan pastikan Anda memanggil hingga base.GetObjectData(info, context)akhir, agar kelas dasar menyimpan keadaannya sendiri.

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

Tes Unit

Unit tes MSTest untuk tiga jenis pengecualian yang ditentukan di atas.

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()");
        }
    }
}
Daniel Fortunov
sumber
3
+1: tetapi jika Anda akan mengalami banyak masalah ini, saya akan berusaha keras dan mengikuti semua pedoman MS untuk menerapkan pengecualian. Yang dapat saya ingat adalah untuk memberikan konstruktor standar MyException (), MyException (pesan string) dan MyException (pesan string, Exception innerException)
Joe
3
Juga - bahwa Kerangka Desain Kerangka Kerja mengatakan bahwa nama untuk pengecualian harus diakhiri dengan "Pengecualian". Sesuatu seperti MyExceptionAndHereIsaQualifyingAdverbialPhrase tidak direkomendasikan. msdn.microsoft.com/en-us/library/ms229064.aspx Seseorang pernah berkata, kode yang kami berikan di sini sering digunakan sebagai pola, jadi kita harus berhati-hati untuk memperbaikinya.
Cheeso
1
Cheeso: Buku "Kerangka Desain Pedoman", di bagian Merancang Pengecualian Kustom, menyatakan: "Berikan (setidaknya) konstruktor umum ini pada semua pengecualian." Lihat di sini: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Hanya konstruktor (info SerializationInfo, konteks StreamingContext) yang diperlukan untuk kebenaran serialisasi, sisanya disediakan untuk menjadikan ini sebagai titik awal yang baik untuk potong dan tempel. Ketika Anda memotong dan menyisipkan, bagaimanapun, Anda pasti akan mengubah nama kelas, oleh karena itu saya tidak berpikir melanggar konvensi pengecualian penamaan signifikan di sini ...
Daniel Fortunov
3
apakah jawaban yang diterima ini juga berlaku untuk .NET Core? Dalam .net core GetObjectDatatidak pernah dipanggil..tetapi saya bisa mengganti ToString()yang dipanggil
LP13
3
Tampaknya ini bukan cara mereka melakukannya di dunia baru. Misalnya, secara harfiah tidak ada pengecualian dalam ASP.NET Core diimplementasikan dengan cara ini. Mereka semua menghilangkan hal-hal serialisasi: github.com/aspnet/Mvc/blob/…
bitbonk
25

Pengecualian sudah serializable, tetapi Anda perlu mengganti GetObjectDatametode untuk menyimpan variabel Anda dan memberikan konstruktor yang dapat dipanggil saat kembali menghidrasi objek Anda.

Jadi contoh Anda menjadi:

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

}
Adrian Clark
sumber
1
Seringkali Anda bisa lolos hanya dengan menambahkan [Serializable] ke kelas Anda.
Hallgrim
3
Hallgrim: Menambahkan [Serializable] tidak cukup jika Anda memiliki bidang tambahan untuk diserialisasi.
Joe
2
NB: "Secara umum konstruktor ini harus dilindungi jika kelas tidak disegel" - jadi konstruktor serialisasi dalam contoh Anda harus dilindungi (atau, mungkin lebih tepat, kelas harus disegel kecuali warisan diperlukan secara khusus). Selain itu, kerja bagus!
Daniel Fortunov
Dua kesalahan lain dalam hal ini: atribut [Serializable] adalah wajib jika tidak serialisasi gagal; GetObjectData harus menghubungi melalui ke base.GetObjectData
Daniel Fortunov
8

Terapkan ISerializable, dan ikuti pola normal untuk melakukan ini.

Anda perlu memberi tag kelas dengan atribut [Serializable], dan menambahkan dukungan untuk antarmuka itu, dan juga menambahkan konstruktor tersirat (dijelaskan pada halaman itu, mencari tersirat konstruktor ). Anda dapat melihat contoh penerapannya dalam kode di bawah teks.

Lasse V. Karlsen
sumber
8

Untuk menambah jawaban yang benar di atas, saya menemukan bahwa saya bisa menghindari melakukan hal ini hal serialisasi kustom jika saya menyimpan properti kustom saya di Datakoleksi dari Exceptionkelas.

Misalnya:

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

    // ...
}

Mungkin ini kurang efisien dalam hal kinerja daripada solusi yang diberikan oleh Daniel dan mungkin hanya bekerja untuk tipe "integral" seperti string dan integer dan sejenisnya.

Tetap saja itu sangat mudah dan sangat dimengerti bagi saya.

Uwe Keim
sumber
1
Ini adalah cara yang bagus dan sederhana untuk menangani informasi pengecualian tambahan dalam kasus di mana Anda hanya perlu menyimpannya untuk masuk atau semacamnya. Jika Anda pernah perlu mengakses nilai-nilai tambahan ini dalam kode di catch-block namun Anda kemudian akan mengandalkan mengetahui kunci untuk nilai data secara eksternal yang tidak baik untuk enkapsulasi dll.
Christopher King
2
Wow Terimakasih. Saya terus secara acak kehilangan semua variabel tambahan kustom saya setiap kali pengecualian digunakan kembali throw;dan ini memperbaikinya.
Nyerguds
1
@ChristopherKing Mengapa Anda perlu mengetahui kunci? Mereka dikodekan dengan rajin rajin.
Nyerguds
1

Dulu ada artikel yang sangat baik dari Eric Gunnerson di MSDN "Pengecualian yang baik hati" tetapi tampaknya telah ditarik. URL-nya adalah:

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

Jawaban Aydsman benar, info lebih lanjut di sini:

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

Saya tidak bisa memikirkan kasus penggunaan untuk Pengecualian dengan anggota non-serializable, tetapi jika Anda menghindari mencoba membuat serial / deserialize mereka di GetObjectData dan konstruktor deserialisasi Anda harus baik-baik saja. Juga tandai mereka dengan atribut [NonSerialized], lebih sebagai dokumentasi daripada yang lain, karena Anda sendiri yang mengimplementasikan serialisasi.

Joe
sumber
0

Tandai kelas dengan [Serializable], meskipun saya tidak yakin seberapa baik anggota IList akan ditangani oleh serializer.

EDIT

Posting di bawah ini benar, karena pengecualian khusus Anda memiliki konstruktor yang mengambil parameter, Anda harus menerapkan ISerializable.

Jika Anda menggunakan konstruktor default dan mengekspos dua anggota kustom dengan properti pengambil / penyetel, Anda bisa pergi hanya dengan mengatur atribut.

David Hill
sumber
-5

Saya harus berpikir bahwa ingin membuat serial pengecualian merupakan indikasi kuat bahwa Anda mengambil pendekatan yang salah untuk sesuatu. Apa tujuan utamanya, di sini? Jika Anda melewatkan pengecualian di antara dua proses, atau di antara proses terpisah dari proses yang sama, maka sebagian besar properti pengecualian tidak akan valid dalam proses lainnya.

Mungkin akan lebih masuk akal untuk mengekstrak informasi status yang Anda inginkan pada pernyataan catch (), dan mengarsipkannya.

Mark Bessey
sumber
9
Downvote - perkecualian pedoman Microsoft seharusnya msdn.microsoft.com/en-us/library/ms229064.aspx serializable agar dapat dilemparkan melintasi batas appdomain, misalnya menggunakan remoting.
Joe