사용자 지정 .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; }
    }
}

이 예외가 직렬화되고 역 직렬화되면 두 개의 사용자 지정 속성 ( ResourceNameValidationErrors)이 유지되지 않습니다. 속성이 반환 null됩니다.

사용자 정의 예외에 대한 직렬화를 구현하기위한 공통 코드 패턴이 있습니까?

답변:


411

사용자 정의 속성이없는 기본 구현

CustomProperties.cs가없는 SerializableException :

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파생 클래스는 사용자 정의 직렬화를 구현해야 함을 의미한다. 여기에는 두 단계가 포함됩니다.
    1. 직렬화 생성자를 제공합니다 . 이 생성자는 private클래스가 sealed인 경우, 그렇지 않은 경우 protected파생 클래스에 대한 액세스를 허용 해야합니다 .
    2. base.GetObjectData(info, context)기본 클래스가 자체 상태를 저장하게하려면 GetObjectData ()를 재정의 하고 마지막에 호출해야합니다 .

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

AdditionalCustomProperties.cs로 DerivedSerializableException :

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

3
+1 : 그러나 이처럼 많은 어려움을 겪고 있다면 예외를 구현하기위한 모든 MS 지침을 따르겠습니다. 내가 기억할 수있는 것은 표준 construcors MyException (), MyException (string message) 및 MyException (string message, Exception innerException)을 제공하는 것입니다.
Joe

3
또한 - 프레임 워크 디자인 Guideliness 예외의 이름을 말할 것을 해야한다 "예외"로 끝납니다. MyExceptionAndHereIsaQualifyingAdverbialPhrase와 같은 것은 권장되지 않습니다. msdn.microsoft.com/ko-kr/library/ms229064.aspx 누군가가 한 번 말씀 드리지만 , 여기에서 제공하는 코드는 종종 패턴으로 사용되므로 올바르게 처리해야합니다.
Cheeso

1
Cheeso : 사용자 지정 예외 디자인 섹션의 "프레임 워크 디자인 지침"책에는 "적어도 모든 예외에 대해 이러한 공통 생성자를 제공하십시오."라고 나와 있습니다. 여기를 참조하십시오 : blogs.msdn.com/kcwalina/archive/2006/07/05/657268.aspx 직렬화 정확성에는 (SerializationInfo info, StreamingContext context) 생성자 만 필요하며, 나머지는이를위한 좋은 시작점으로 제공됩니다. 잘라 붙여 넣기. 그러나 잘라서 붙여 넣을 때 클래스 이름을 반드시 변경하므로 예외 명명 규칙을 위반하는 것이 중요하지 않다고 생각합니다.
Daniel Fortunov

3
이 답변이 .NET Core에도 해당됩니까? .net 코어에서 GetObjectData절대 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

3
Hallgrim : 직렬화 할 필드가 더 있으면 [Serializable]을 추가하는 것만으로는 충분하지 않습니다.
Joe

2
NB : "일반적으로이 생성자는 클래스가 봉인되지 않은 경우 보호되어야합니다"– 예제의 직렬화 생성자가 보호되어야합니다 (또는 상속이 특별히 필요하지 않은 경우 클래스가 봉인되어야 함). 그 외에는 잘하셨습니다!
Daniel Fortunov

이에 대한 두 가지 다른 실수 : [직렬화 가능] 속성은 필수입니다. 그렇지 않으면 직렬화에 실패합니다. GetObjectData는 base.GetObjectData를 통해 호출해야합니다
다니엘 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
이는 로깅 또는 이와 유사한 정보를 저장하기 위해 필요한 경우에만 추가 예외 정보를 처리 할 수있는 훌륭하고 간단한 방법입니다. 그러나 catch 블록에서 코드로 이러한 추가 값에 액세스해야하는 경우 외부 적으로 데이터 값의 키를 알고 있어야 캡슐화 등에 적합하지 않습니다.
Christopher King

2
우와, 고마워요. 예외를 다시 사용하여 throw;수정했을 때마다 모든 사용자 정의 추가 변수가 임의로 손실되었습니다 .
Nyerguds

1
@ChristopherKing 왜 열쇠를 알아야합니까? 그들은 게터에 하드 코딩되어 있습니다.
Nyerguds

1

에릭 건너 슨 (Eric Gunnerson)의 MSDN "우수한 예외"에 대한 훌륭한 기사가 있었지만 풀려 난 것 같습니다. URL은 다음과 같습니다

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

Aydsman의 대답은 정확하고 자세한 정보는 다음과 같습니다.

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

직렬화 할 수없는 멤버의 예외에 대한 유스 케이스는 생각할 수 없지만 GetObjectData 및 직렬화 해제 생성자에서 직렬화 / 직렬화 해제를 시도하지 않으면 괜찮을 것입니다. 또한 직렬화를 직접 구현하고 있으므로 [NonSerialized] 속성을 다른 것보다 문서로 표시하십시오.


0

직렬화 기가 IList 멤버를 얼마나 잘 처리하는지 잘 모르겠지만 클래스를 [Serializable]로 표시하십시오.

편집하다

사용자 지정 예외에 매개 변수를 사용하는 생성자가 있으므로 아래 게시물이 정확합니다. ISerializable을 구현해야합니다.

기본 생성자를 사용하고 getter / setter 속성이있는 두 개의 사용자 지정 멤버를 노출 한 경우 속성을 설정하기 만하면 벗어날 수 있습니다.


-5

예외를 직렬화하려는 것이 잘못된 접근 방식을 취하고 있다는 강력한 표시라고 생각해야합니다. 여기서 궁극적 인 목표는 무엇입니까? 두 프로세스 간 또는 동일한 프로세스의 개별 실행간에 예외를 전달하는 경우 예외의 대부분의 속성은 다른 프로세스에서 유효하지 않습니다.

catch () 문에서 원하는 상태 정보를 추출하여 보관하는 것이 더 합리적 일 것입니다.


9
Downvote-Microsoft 지침에 따라 예외는 직렬화 가능해야합니다. msdn.microsoft.com/en-us/library/ms229064.aspx 따라서 원격으로 사용하는 등 응용 프로그램 도메인 경계를 넘어서 발생할 수 있습니다.
Joe
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.