如何正確實現一個自定義 Exception

2023-09-04 06:01:17

最近在公司的專案中,編寫了幾個自定義的 Exception 類。提交 PR 的時候,sonarqube 提示這幾個自定義異常不符合 ISerializable patten. 花了點時間稍微研究了一下,把這個問題解了。今天在此記錄一下,可能大家都會幫助到大家。

自定義異常

編寫一個自定義的異常,繼承自 Exception,其中定義一個 ErrorCode 來儲存異常編號。平平無奇的一個類,太常見了。大家覺得有沒有什麼問題?

    [Serializable]
    public class MyException : Exception
    {
        public string ErrorCode { get;}

        public MyException(string message, string errorCode) : base(message)
        {
            ErrorCode = errorCode;
        }
    }

如我們對這個異常編寫一個簡單的單元測試。步驟如下:

        [TestMethod()]
        public void MyExceptionTest()
        {
            // arrange
            var orignalException = new MyException("Hi", "1000");
            var bf = new BinaryFormatter();
            var ms = new MemoryStream();

            // act
            bf.Serialize(ms, orignalException);
            ms.Seek(0, 0);
            var newException = bf.Deserialize(ms) as MyException;

            // assert
            Assert.AreEqual(orignalException.Message, newException.Message);
            Assert.AreEqual(orignalException.ErrorCode, newException.ErrorCode);
        }

這個測試主要是對一個 MyException 的範例使用 BinaryFormatter 進行序列化,然後反序列化成一個新的物件。將新舊兩個物件的 ErrorCodeMessage 欄位進行斷言,也很簡單。
讓我們執行一下這個測試,很可惜失敗了。測試用例直接拋了一個異常,大概是說找不到序列化構造器。

Designing Custom Exceptions Guideline

簡單的搜尋了一下,發現微軟有對於自定義 Exception 的
Designing Custom Exceptions

總結一下大概有以下幾點:

  • 一定要從 System.Exception 或其他常見基本異常之一派生異常。

  • 異常類名稱一定要以字尾 Exception 結尾。

  • 應使異常可序列化。 異常必須可序列化才能跨越應用程式域和遠端處理邊界正確工作。

  • 一定要在所有異常上都提供(至少是這樣)下列常見建構函式。 確保引數的名稱和型別與在下面的程式碼範例中使用的那些相同。

public class NewException : BaseException, ISerializable
{
    public NewException()
    {
        // Add implementation.
    }
    public NewException(string message)
    {
        // Add implementation.
    }
    public NewException(string message, Exception inner)
    {
        // Add implementation.
    }

    // This constructor is needed for serialization.
   protected NewException(SerializationInfo info, StreamingContext context)
   {
        // Add implementation.
   }
}

按照上面的 guideline 重新改一下我們的 MyException,主要是新增了幾個構造器。修改後的程式碼如下:

    [Serializable]
    public class MyException : Exception
    {
        public string ErrorCode { get; }

        public MyException()
        {
        }

        public MyException(string message, string errorCode) : base(message)
        {
            ErrorCode = errorCode;
        }

        public MyException(string message, Exception inner): base(message, inner)
        {
        }

        protected MyException(SerializationInfo info, StreamingContext context)
        {
        }

    }

很可惜按照微軟的 guideline 單元測試還是沒通過。獲取 Message 欄位的時候會直接 throw 一個 Exception。

那麼到底該怎麼實現呢?

正確的方式

我們還是按照微軟 guideline 進行編寫,但是在序列化構造器的上呼叫 base 的構造器。並且 override 基礎類別的 GetObjectData 方法。

    [Serializable]
    public class MyException : Exception
    {
        public string ErrorCode { get; }

        public MyException()
        {
        }

        public MyException(string message, string errorCode) : base(message)
        {
            ErrorCode = errorCode;
        }

        public MyException(string message, Exception inner): base(message, inner)
        {
        }

        protected MyException(SerializationInfo info, StreamingContext context): base(info, context)
        {
            // Set the ErrorCode value from info dictionary.
            ErrorCode = info.GetString("ErrorCode");
        }

        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (!string.IsNullOrEmpty(ErrorCode))
            {
                // Add the ErrorCode to the SerializationInfo dictionary.
                info.AddValue("ErrorCode", ErrorCode);
            }
            base.GetObjectData(info, context);
        }
    }

在序列化構造器裡從 SerializationInfo 物件裡恢復 ErrorCode 的值。呼叫 base 的構造可以確保基礎類別的 Message 欄位被正確的還原。這裡與其說是序列化構造器不如說是反序列化構造器,因為這個構造器會在反序列化恢復成物件的時候被呼叫。

   protected MyException(SerializationInfo info, StreamingContext context): base(info, context)
        {
            // Set the ErrorCode value from info dictionary.
            ErrorCode = info.GetString("ErrorCode");
        }

這個 GetObjectData 方法是 ISerializable 介面提供的方法,所以基礎類別裡肯定有實現。我們的子類需要 override 它。把自己需要序列化的欄位新增到 SerializationInfo 物件中,這樣在上面反序列化的時候確保可以把欄位的值給恢復回來。記住不要忘記呼叫 base.GetObjectData(info, context), 確保基礎類別的欄位資料能正確的被序列化。

    public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (!string.IsNullOrEmpty(ErrorCode))
            {
                // Add the ErrorCode to the SerializationInfo dictionary.
                info.AddValue("ErrorCode", ErrorCode);
            }
            base.GetObjectData(info, context);
        }

再次執行單元測試,這次順利的通過了