Özel bir .NET Exception'ı serileştirilebilir hale getirmenin doğru yolu nedir?


225

Daha spesifik olarak, istisna kendileri serileştirilebilen veya serileştirilemeyen özel nesneler içerdiğinde.

Bu örneği ele alalım:

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

Bu Kural Dışı Durum serileştirilmiş ve serileştirilmişse, iki özel özellik ( ResourceNameve ValidationErrors) korunmaz. Özellikler dönecektir null.

Özel istisna için serileştirmeyi uygulamak için ortak bir kod deseni var mı?

Yanıtlar:


412

Özel özellikler olmadan temel uygulama

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

Özel özelliklerle tam uygulama

Özel bir seri hale getirilebilir istisna (tam uygulama MySerializableException) ve türetilmiş bir sealedistisna ( MyDerivedSerializableException).

Bu uygulamanın ana noktaları burada özetlenmiştir:

  1. Sen her türetilmiş sınıf süslemeleri gerekir [Serializable]özniteliği - Bu özellik temel sınıfından miras değildir ve bunun belirtilmemiş ise, seri hale bir başarısız olur SerializationExceptionbelirten "Montaj Y X Tipi seri hale olarak işaretlenmemiş."
  2. Sen özel seri uygulamalıdır . [Serializable]Yalnız nitelik yeterli değildir - Exceptionuygular ISerializableTüretilmiş sınıfları da özel seri uygulamalıdır anlamına gelir. Bu iki adımdan oluşur:
    1. Bir serileştirme yapıcısı sağlayın . Bu yapıcı privatesınıfınız sealedolmalıdır, aksi takdirde protectedtüretilmiş sınıflara erişime izin vermelidir.
    2. GetObjectData () öğesini geçersiz kılınbase.GetObjectData(info, context) ve temel sınıfın kendi durumunu kaydetmesine izin vermek için sonuna kadar aradığınızdan emin olun .

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

Birim Testleri

Yukarıda tanımlanan üç istisna türü için MSTest birim testleri.

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: ancak bu kadar sorun çıkarırsanız, tüm yol boyunca gidip istisnaları uygulamak için tüm MS yönergelerini izlerdim. Hatırlayabildiğim bir şey, standart yapıları MyException (), MyException (dize mesajı) ve MyException (dize mesajı, Exception innerException) sağlamaktır
Joe

3
Ayrıca - Çerçeve Tasarım Kılavuzluğu, istisnalar için adların "İstisna" ile bitmesi gerektiğini söylüyor . MyExceptionAndHereIsaQualifyingAdverbialPhrase gibi bir şey önerilmez. msdn.microsoft.com/tr-tr/library/ms229064.aspx Birisi bir zamanlar söylediğimizde, burada verdiğimiz kod genellikle bir desen olarak kullanılır, bu yüzden doğru yapmak için dikkatli olmalıyız.
Cheeso

1
Cheeso: Özel İstisnaları Tasarlama bölümünde yer alan "Çerçeve Tasarım Yönergeleri" kitabı şunları söylüyor: "Tüm bu istisnalar için bu ortak kurucuları (en azından) sağlayın." Buraya bakın: blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Serileştirme doğruluğu için yalnızca (SerializationInfo bilgisi, StreamingContext bağlamı) yapıcısına ihtiyaç vardır, gerisi bunun için iyi bir başlangıç ​​noktasıdır. kes ve yapıştır. Ancak kesip yapıştırdığınızda, kesinlikle sınıf isimlerini değiştireceksiniz, bu yüzden istisna adlandırma kuralını ihlal etmenin burada önemli olduğunu düşünmüyorum ...
Daniel Fortunov

3
kabul edilen cevap .NET Core için de geçerli mi? .Net çekirdek olarak GetObjectDatai geçersiz kılabilirsiniz invoked..however geçmez ToString()çağrılan alır ki
LP13

3
Görünüşe göre bu yeni dünyada böyle yapılmıyor. Örneğin, ASP.NET Core'da tam anlamıyla bir istisna bu şekilde uygulanmaz. Hepsi serileştirmeyi atlıyor
bitbonk

25

İstisna zaten serileştirilebilir, ancak GetObjectDatadeğişkenlerinizi saklamak ve nesneyi yeniden nemlendirirken çağrılabilecek bir yapıcı sağlamak için yöntemi geçersiz kılmanız gerekir.

Böylece örneğin:

[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
Genellikle sadece [Serializable] sınıfınıza ekleyerek kurtulabilirsiniz.
Hallgrim

3
Hallgrim: Serileştirilecek ek alanlarınız varsa [Serileştirilebilir] eklemek yeterli değildir.
Joe

2
Not: "Genel olarak bu yapıcı sınıf mühürlenmemişse korunmalıdır" - bu nedenle örneğinizdeki serileştirme kurucusu korunmalıdır (veya miras özellikle gerekli olmadıkça sınıf daha iyi kapatılmalıdır). Bunun dışında iyi iş çıkardın!
Daniel Fortunov

Bunda iki hata daha var: [Serializable] özelliği zorunludur, aksi takdirde serileştirme başarısız olur; GetObjectData base.GetObjectData aracılığıyla aramak gerekir
Daniel Fortunov

8

ISerializable uygulayın ve bunu yapmak için normal modeli izleyin .

Sınıfı [Serializable] özniteliğiyle etiketlemeniz ve bu arabirim için destek eklemeniz ve ayrıca zımni yapıcıyı eklemeniz gerekir (bu sayfada açıklanan, bir yapıcı anlamına gelir ). Uygulamanın bir örneğini metnin altındaki kodda görebilirsiniz.


8

Yukarıdaki doğru cevaplara eklemek için , sınıfımın Datakoleksiyonundaki özel özelliklerimi saklarsam bu özel serileştirme işlerini yapmaktan kaçınabileceğimi keşfettim Exception.

Örneğin:

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

    // ...
}

Muhtemelen bu, performans açısından Daniel tarafından sağlanan çözümden daha az verimlidir ve muhtemelen sadece dizeler ve tamsayılar ve benzeri gibi "integral" tipler için çalışır.

Yine de benim için çok kolay ve anlaşılır bir durumdu.


1
Bu, yalnızca günlük kaydı veya başka bir şey için saklamanız gereken durumlarda ek istisna bilgilerini işlemenin güzel ve basit bir yoludur. Koddaki bu ek değerlere catch bloğunda erişmeniz gerekiyorsa, ancak kapsülleme vb. İçin iyi olmayan veri değerlerinin anahtarlarını bilmeye güveniyor olacaksınız
Christopher King

2
Vay canına, teşekkür ederim. Her ne zaman bir istisna kullanılarak yeniden oluşturulduğunda tüm özel eklenen değişkenlerimi rastgele kaybettim throw;ve bu düzeltti.
Nyerguds

1
@ChristopherKing Anahtarları neden bilmeniz gerekiyor? Alıcıda sabit kodlanmışlar.
Nyerguds

1

Eric Gunnerson'ın "İyi huylu istisna" adlı MSDN'de mükemmel bir makalesi vardı ama çekildi gibi görünüyor. URL:

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

Aydsman'ın cevabı doğru, daha fazla bilgi burada:

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

Serileştirilemeyen üyelere sahip bir İstisna için herhangi bir kullanım durumunu düşünemiyorum, ancak GetObjectData ve serileştirme kurucusunda serileştirmeye / serileştirmeyi denemekten kaçınsanız, Tamam olmalısınız. Ayrıca, serileştirmeyi kendiniz uyguladığınızdan, bunları [Her şeyden daha fazla dokümantasyon olarak [Seri Olmayan] özniteliğiyle işaretleyin.


0

Bir IList üyesinin serileştirici tarafından ne kadar iyi ele alınacağından emin olmasam da, sınıfı [Serializable] ile işaretleyin.

DÜZENLE

Aşağıdaki gönderi doğrudur, çünkü özel kural dışı durumunuz parametreleri alan bir kurucuya sahip olduğundan, ISerializable uygulamanız gerekir.

Varsayılan bir kurucu kullandıysanız ve iki özel üyeyi getter / setter özelliklerine maruz bıraktıysanız, yalnızca özelliği ayarlayarak kurtulabilirsiniz.


-5

Bir istisnayı serileştirmek istemenin, bir şeye yanlış yaklaşımı aldığınızı gösteren güçlü bir gösterge olduğunu düşünmeliyim. Burada nihai hedef nedir? İstisna iki işlem arasında veya aynı işlemin ayrı çalıştırmaları arasında geçiyorsa, istisnanın özelliklerinin çoğu yine de diğer işlemde geçerli olmayacaktır.

Catch () deyiminde istediğiniz durum bilgisini ayıklamak ve arşivlemek muhtemelen daha mantıklı olacaktır.


9
Downvote - Microsoft yönergeleri durum istisnaları serileştirilebilir olmalıdır msdn.microsoft.com/en-us/library/ms229064.aspx Böylece bir uygulama sınırında, örneğin uzaktan kumanda kullanılarak atılabilir.
Joe
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.