Cách chính xác để tạo một phiên bản .NET Exception tùy chỉnh là gì?


225

Cụ thể hơn, khi ngoại lệ chứa các đối tượng tùy chỉnh có thể tự hoặc không thể tuần tự hóa.

Lấy ví dụ này:

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

Nếu Ngoại lệ này được tuần tự hóa và khử nối tiếp, hai thuộc tính tùy chỉnh ( ResourceNameValidationErrors) sẽ không được giữ nguyên. Các tài sản sẽ trở lại null.

Có một mẫu mã phổ biến để thực hiện tuần tự hóa cho ngoại lệ tùy chỉnh không?

Câu trả lời:


411

Cơ sở thực hiện, không có thuộc tính tùy chỉnh

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

Thực hiện đầy đủ, với các thuộc tính tùy chỉnh

Hoàn thành thực hiện một ngoại lệ tuần tự hóa tùy chỉnh ( MySerializableException) và sealedngoại lệ xuất phát ( MyDerivedSerializableException).

Những điểm chính về việc thực hiện này được tóm tắt ở đây:

  1. Bạn phải trang trí cho mỗi lớp dẫn xuất bằng [Serializable]thuộc tính - Thuộc tính này không được kế thừa từ lớp cơ sở và nếu không được chỉ định, việc xê-ri hóa sẽ thất bại với thông SerializationExceptionbáo rằng "Loại X trong hội Y không được đánh dấu là tuần tự hóa".
  2. Bạn phải thực hiện tuần tự hóa tùy chỉnh . Chỉ [Serializable]riêng thuộc tính là không đủ - Exceptionthực hiện ISerializableđiều đó có nghĩa là các lớp dẫn xuất của bạn cũng phải thực hiện tuần tự hóa tùy chỉnh. Điều này bao gồm hai bước:
    1. Cung cấp một hàm tạo tuần tự hóa . Hàm tạo này phải là privatenếu lớp của bạn sealed, nếu không nó sẽ protectedcho phép truy cập vào các lớp dẫn xuất.
    2. Ghi đè GetObjectData () và đảm bảo rằng bạn gọi đến base.GetObjectData(info, context)cuối, để cho lớp cơ sở lưu trạng thái của chính nó.

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

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

Bài kiểm tra đơn vị

Kiểm tra đơn vị MSTest cho ba loại ngoại lệ được xác định ở trên.

Đơn vị.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: nhưng nếu bạn gặp phải nhiều rắc rối này, tôi sẽ tìm mọi cách và làm theo tất cả các hướng dẫn của MS để thực hiện các ngoại lệ. Một điều tôi có thể nhớ là cung cấp các trình xây dựng tiêu chuẩn MyException (), MyException (thông báo chuỗi) và MyException (thông báo chuỗi, Exception InternalException)
Joe

3
Ngoài ra - rằng Guideliness của Thiết kế khung nói rằng các tên cho các trường hợp ngoại lệ nên kết thúc bằng "Ngoại lệ". Một cái gì đó như MyExceptionAndHereIsaQualifyingAdverbialPhrase không được khuyến khích. msdn.microsoft.com/en-us/l Library / ms229064.aspx Ai đó đã từng nói, mã chúng tôi cung cấp ở đây thường được sử dụng làm mẫu, vì vậy chúng tôi nên cẩn thận để làm cho đúng.
Cheeso

1
Cheeso: Cuốn sách "Nguyên tắc thiết kế khung", trong phần Thiết kế ngoại lệ tùy chỉnh, nêu rõ: "Đừng cung cấp (ít nhất) các hàm tạo phổ biến này cho tất cả các ngoại lệ." Xem tại đây: blog.msdn.com/kcwalina/archive/2006/07/05/657268.aspx Chỉ có hàm tạo (serializationInfo, bối cảnh StreamingContext) là cần thiết cho tính chính xác của tuần tự hóa, phần còn lại được cung cấp để biến đây thành điểm khởi đầu tốt cho cắt và dán. Tuy nhiên, khi bạn cắt và dán, bạn chắc chắn sẽ thay đổi tên lớp, do đó tôi không nghĩ việc vi phạm quy ước đặt tên ngoại lệ có ý nghĩa ở đây ...
Daniel Fortunov

3
câu trả lời được chấp nhận này có đúng với .NET Core không? Trong lõi .net GetObjectDatakhông bao giờ được gọi .. bất cứ khi nào tôi có thể ghi đè ToString()được gọi
LP13

3
Có vẻ như đây không phải là cách họ được thực hiện trong thế giới mới. Ví dụ, theo nghĩa đen, không có ngoại lệ nào trong ASP.NET Core được triển khai theo cách này. Tất cả đều bỏ qua các công cụ tuần tự hóa: github.com/aspnet/Mvc/blob/,
bitbonk

25

Ngoại lệ đã được tuần tự hóa, nhưng bạn cần ghi đè GetObjectDataphương thức để lưu trữ các biến của mình và cung cấp một hàm tạo có thể được gọi khi hydrat hóa lại đối tượng của bạn.

Vì vậy, ví dụ của bạn trở thành:

[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
Thường thì bạn có thể thoát khỏi việc chỉ cần thêm [Nối tiếp] vào lớp.
Hallgrim

3
Hallgrim: Thêm [Nối tiếp] là không đủ nếu bạn có các trường bổ sung để tuần tự hóa.
Joe

2
Lưu ý Ngoài ra, công việc tốt!
Daniel Fortunov

Hai lỗi khác trong trường hợp này: thuộc tính [Nối tiếp] là bắt buộc nếu không việc tuần tự hóa không thành công; GetObjectData phải gọi qua căn cứ.GetObjectData
Daniel Fortunov

8

Thực hiện ISerializable và theo mô hình bình thường để làm điều này.

Bạn cần gắn thẻ lớp với thuộc tính [Nối tiếp] và thêm hỗ trợ cho giao diện đó, đồng thời thêm hàm tạo hàm ý (được mô tả trên trang đó, tìm kiếm hàm ý hàm tạo ). Bạn có thể thấy một ví dụ về việc thực hiện nó trong mã bên dưới văn bản.


8

Để thêm vào các câu trả lời đúng ở trên, tôi phát hiện ra rằng tôi có thể tránh thực hiện công cụ tuần tự hóa tùy chỉnh này nếu tôi lưu trữ các thuộc tính tùy chỉnh của mình trong Databộ sưu tập của Exceptionlớp.

Ví dụ:

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

    // ...
}

Có lẽ điều này kém hiệu quả về mặt hiệu suất so với giải pháp do Daniel cung cấp và có lẽ chỉ hoạt động đối với các loại "tích phân" như chuỗi và số nguyên và tương tự.

Tuy nhiên, nó rất dễ dàng và rất dễ hiểu đối với tôi.


1
Đây là một cách hay và đơn giản để xử lý thông tin ngoại lệ bổ sung trong trường hợp bạn chỉ cần lưu trữ để ghi nhật ký hoặc đại loại như thế. Nếu bạn cần truy cập các giá trị bổ sung này trong mã trong một khối bắt, tuy nhiên sau đó bạn sẽ dựa vào việc biết các khóa cho các giá trị dữ liệu bên ngoài, điều này không tốt cho việc đóng gói, v.v.
Christopher King

2
Ồ, cảm ơn bạn. Tôi đã mất ngẫu nhiên tất cả các biến được thêm tùy chỉnh của mình bất cứ khi nào một ngoại lệ được sử dụng lại throw;và điều này đã sửa nó.
Nyerguds

1
@ChristopherKing Tại sao bạn cần biết các phím? Họ đã được mã hóa cứng trong getter.
Nyerguds

1

Đã từng có một bài viết xuất sắc từ Eric Gunnerson trên MSDN "Ngoại lệ nóng nảy" nhưng dường như nó đã được rút ra. URL là:

http://msdn.microsoft.com/l Library / default.asp? url = / l Library / en-us / dcsccol / html / csharp08162001.asp

Câu trả lời của Aydsman là chính xác, thông tin thêm ở đây:

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

Tôi không thể nghĩ ra bất kỳ trường hợp sử dụng nào cho Ngoại lệ với các thành viên không tuần tự hóa, nhưng nếu bạn tránh cố gắng tuần tự hóa / giải tuần tự hóa chúng trong GetObjectData và trình xây dựng giải tuần tự hóa thì bạn sẽ ổn. Đồng thời đánh dấu chúng bằng thuộc tính [NonSerialized], dưới dạng tài liệu hơn bất kỳ thứ gì khác, vì bạn đang tự thực hiện tuần tự hóa.


0

Đánh dấu lớp bằng [serializable], mặc dù tôi không chắc thành viên IList sẽ được xử lý bởi serializer như thế nào.

BIÊN TẬP

Bài viết dưới đây là chính xác, vì ngoại lệ tùy chỉnh của bạn có hàm tạo có tham số, bạn phải triển khai ISerializable.

Nếu bạn đã sử dụng một hàm tạo mặc định và hiển thị hai thành viên tùy chỉnh với các thuộc tính getter / setter, bạn có thể thoát khỏi chỉ bằng cách đặt thuộc tính.


-5

Tôi phải nghĩ rằng muốn nối tiếp một ngoại lệ là một dấu hiệu mạnh mẽ cho thấy bạn đang sử dụng sai phương pháp tiếp cận một cái gì đó. Mục tiêu cuối cùng là gì? Nếu bạn chuyển ngoại lệ giữa hai quy trình hoặc giữa các lần chạy riêng biệt của cùng một quy trình, thì hầu hết các thuộc tính của ngoại lệ sẽ không có hiệu lực trong quy trình khác.

Có lẽ sẽ có ý nghĩa hơn khi trích xuất thông tin trạng thái bạn muốn tại câu lệnh Catch () và lưu trữ thông tin đó.


9
Downvote - các ngoại lệ trạng thái hướng dẫn của Microsoft phải được tuần tự hóa msdn.microsoft.com/en-us/l Library / ms229064.aspx Vì vậy, chúng có thể được ném qua một ranh giới tên miền appd, ví dụ như sử dụng từ xa.
Joe
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.