最近在公司的專案中,編寫了幾個自定義的 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
欄位進行斷言,也很簡單。
讓我們執行一下這個測試,很可惜失敗了。測試用例直接拋了一個異常,大概是說找不到序列化構造器。
簡單的搜尋了一下,發現微軟有對於自定義 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);
}
再次執行單元測試,這次順利的通過了