多型在像是 C#、爪哇(用中文應該就不會被告了)這種強型別的物件導向程式語言,要將一個抽象類別的實例存到關聯式資料庫或是做序列化/反序列化,都需要另外處理,關於多型跟關聯式資料庫的對應方法我已經寫在這篇文章裡面,這篇要來寫寫多型如何做 JSON 的序列化/反序列化。
假設我定義了一個 Food
抽象類別,派生了三個子類分別是:Dessert
、DryGoods
、Delicatessen
。
一般解法
用 Json.NET 做多型的序列化只要在序列化的時候,指定 JsonSerializerSettings
的 TypeNameHandling
屬性為 TypeNameHandling.Objects
。
這麼做就會在序列化的結果多一個 $type
屬性,內容就是該 Type 的 AssemblyQualifiedName
,預設 Json.NET 會移除掉一些 Assembly 的詳細內容。
所以,可想而知當要反序列化的時候,$type 也需要包含進來, 而且要是正確的內容才有辦法反序列化回來。
自訂 SerializationBinder
一般解法有一些限制,如果需要在不同專案之間序列化/反序列化,就要參考相同的 Assembly,而且當資料要存到資料庫去的時候,一般不會將 AssemblyQualifiedName 完整地塞進資料庫去,但是用來識別派生類的欄位還是會有的,現在假設 Food 在資料庫內的資料是這個樣子。
有一個 ShoppingCart
類別,裡面有一個 Foods
屬性,型別是 List<Food>
。
public class ShoppingCart
{
public int Id { get; set; }
public List<Food> Foods { get; set; }
public DateTime CreatedTime { get; set; }
}
然後 ShoppingCart 在資料庫內存放的資料長這樣
我需要從資料庫中一個查詢就能取得 Id 為 1 的 ShoppingCart,並且連同購物車內的所有食物商品也一併取得,因此我利用 SQL Server 的 FOR JSON PATH
語法直接將 Foods 拼成 JSON 回傳。
SELECT
sc.Id
,sc.CreatedTime
,(SELECT
f.Discriminator AS '$type'
,f.Id
,f.[Name]
,f.ShelfLife_Months AS 'ShelfLife.Months'
,f.ShelfLife_Days AS 'ShelfLife.Days'
,f.Dessert_Calorie AS Calorie
,f.DryGoods_CountryOfOrigin AS CountryOfOrigin
,f.Delicatessen_Chef AS Chef
FROM Food f WITH (NOLOCK)
INNER JOIN (SELECT
[value]
FROM STRING_SPLIT(sc.Foods, ',')) sp
ON f.Id = CAST(sp.[value] AS INT)
FOR JSON PATH)
AS Foods
FROM ShoppingCart sc WITH (NOLOCK)
WHERE sc.Id = 1
我識別派生類別的方式是看 Discriminator
欄位的值,配合 Json.NET 的要求,需要將識別的欄位名稱改為 $type,底下為執行結果。
再來我們要實作 ISerializationBinder
介面,自訂 $type 的對應規則。
public class DerivedTypesBinder<TBase> : ISerializationBinder
{
private readonly Dictionary<string, Type> derivedTypes;
public DerivedTypesBinder()
{
var baseType = typeof(TBase);
this.derivedTypes = Assembly.GetAssembly(baseType)
.GetTypes()
.Where(t => t.IsSubclassOf(baseType))
.ToDictionary(x => x.Name, x => x);
}
public Type BindToType(string assemblyName, string typeName)
{
return this.derivedTypes[typeName];
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
}
}
使用方式很簡單,在反序列化時除了指定 JsonSerializerSettings 的 TypeNameHandling 屬性為 TypeNameHandling.Objects 之外,還需要指定 SerializationBinder
屬性。
最後搭配我先前文章提到 Dapper 的 TypeHandler 來使出組合技
public class JsonObjectTypeHandler<T> : SqlMapper.TypeHandler<T>
{
private readonly JsonSerializerSettings serializerSettings;
public JsonObjectTypeHandler(ISerializationBinder serializationBinder = null)
{
if (serializationBinder != null)
{
this.serializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
SerializationBinder = serializationBinder
};
}
}
public override void SetValue(IDbDataParameter parameter, T value)
{
parameter.Value = (value == null) ? (object)DBNull.Value : JsonConvert.SerializeObject(value);
parameter.DbType = DbType.String;
}
public override T Parse(object value)
{
return this.serializerSettings != null
? JsonConvert.DeserializeObject<T>((string)value, this.serializerSettings)
: JsonConvert.DeserializeObject<T>((string)value);
}
}
拜工具進步所賜,現在處理這種多型資料比起以前要容易得多了,希望本篇文章對大家在處理多型資料上有幫助。