最近在公司的項目中,編寫了幾個自定義的 Exception 類。提交 PR 的時候,sonarqube 提示這幾個自定義異常不符合 ISerializable patten. 花了點時間稍微研究了一下,把這個問題解了。今天在此記錄一下,可能大家都會幫助到大家。 ## 自定義異常 編寫一個自定義的異常 ...
最近在公司的項目中,編寫了幾個自定義的 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
進行序列化,然後反序列化成一個新的對象。將新舊兩個對象的 ErrorCode
跟 Message
欄位進行斷言,也很簡單。
讓我們運行一下這個測試,很可惜失敗了。測試用例直接拋了一個異常,大概是說找不到序列化構造器。
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);
}
再次運行單元測試,這次順利的通過了