如何在WebAPI中正確解析(反序列化)抽象類別之子類別實體
前言
有時候為求資料型態的一致,會想要使用抽象類別作為WebApi接收資料的型態;舉個簡單的例子,筆者想要做一個專門註冊使用者的Web API,而我有數個繼承自User抽象類別之使用者類別(試用者、一般使用者及VIP使用者等),各類別會依照使用者類型的不同擁有不同屬性,示意類別如下。
// 使用者
public abstract class User
{
public string Id { get; set; }
public string Name { get; set; }
}
// 試用使用者
public class TrialUser : User
{
// 試用有效期間
public string TrialPeriod {get; set;}
}
// 一般使用者
public class NormalUser : User
{
// 通訊方式
public string PhoneNo { get; set; }
}
// VIP使用者
public class VipUser : User
{
// VIP編號
public string VipNo { get; set; }
}
WebAPI約略如下,使用抽象類別User作為資料傳遞型態,處理所有繼承自該抽象類別的使用者註冊事宜。
public class AccountController : ApiController
{
// 顯示所有帳號資訊
public IEnumerable<User> Get()
{
return new List<User>() {
new TrialUser() { Id="001", Name="Chris", TrialPeriod="102~103" },
new NormalUser() { Id="002", Name="Benny", PhoneNo="0932838485" },
new VipUser() { Id="003", Name="Bob", VipNo="V10293" }
};
}
// 註冊使用者
public void Post([FromBody]User user)
{
// save user here
// 1. trial user, 2. normal user, 3. vip user
// ...
}
}
隨便POST一筆TrialUser資料至WebApi中
結果可想而知為Null,因為JsonSerializer根本不知道要生成哪個子類別實體來轉換所接收到的JSON物件
那要如何來實現這個功能呢? 以下將歸納2種比較常見的方法,可以依照個人需求使用。
解決方案
1. 利用JSON序列化之TypeNameHandling設定進行定義
使用方式相當簡單,只要在WebApiConfig中明確設定是否要在序列化過程中一併處理(識別)類別名稱即可。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// ...
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling =
TypeNameHandling.Objects;
}
}
當我們使用GET方式取出所有User資料時,明顯看出JSON結構中多了型別的資訊
也就因為此特性,所以我們可以在POST資料時明確指出類別的名稱,讓後端收到資料時建立相對應之類別實體,進而達到我們所希望的效果。測試如下,POST一筆TrialUser資料至WebApi中。
確實建立TrialUser實體且資料正確無誤
再測試一筆,這次POST一筆VipUser資料至WebApi中
這次也確實建立VipUser實體且資料正確無誤
此方式確實可以快速地達到我們的需求,但是大家應該也發現了該type資料明確地洩漏出該類別物件的完整Namespace及組件名稱,如果此WepApi是內部使用且無對外開放是可以勉強接受;但若是對外開放則需考量是否合宜,或可選擇使用以下介紹的第二種方式進行。
2. 自訂Customer Json Converter來定義物件實體生成邏輯
簡單的說就是在接收到JSON物件時,透過自行定義的規則(邏輯)解析JSON物件與欲建立之物件實體類別之關係,即可將符合規則之物件實體類別順利產出;在此我們將利用一個JSON物件中TypeName屬性來協助我們判斷待生成物件之類別。
首先需要建立一個繼承JsonConverter之泛型Converter類別,可提高後續使用之便捷性;其中Create方法就是產生物件實體邏輯所在,需依照實際需求來實作這個方法。
public abstract class JsonCreationConverter<T> : JsonConverter
{
// 產生物件實體邏輯(須依實際轉換需求進行實作)
// objectType: ReadJson中預期的物件實體類別型態
// jObject: 讀取到的JOSN物件
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanWrite
{
get
{
return false;
}
}
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on JObject
T target = Create(objectType, jObject);
// Populate the object properties
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
// 在產生物件實體時不會用到
throw new NotImplementedException();
}
}
在抽象類別中加入UserCustomConverter類別,此類別繼承JsonCreationConverter<User>並需要實作Create方法,而方法內主要就是依據JSON物件中Typename屬性資訊來判斷需產生的實體類別;最後在抽象類別User上加註JsonConverter標籤,指定使用UserCustomConverter來進行轉換。
[JsonConverter(typeof(UserCustomConverter))]
public abstract class User
{
public string Id { get; set; }
public string Name { get; set; }
// 定義客製化JSON Converter
private class UserCustomConverter : JsonCreationConverter<User>
{
// 產生物件實體邏輯(依實際轉換需求進行實作)
protected override User Create(Type objectType, JObject jObject)
{
// 讀取JSON後產生對應物件實體
// 此處透過自訂Typename屬性內容判斷生成實體類別
switch (jObject.Value<string>("Typename"))
{
case "TrialUser":
return new TrialUser();
case "NormalUser":
return new NormalUser();
case "VipUser":
return new VipUser();
default:
return new TrialUser();
}
}
}
}
接著測試一下該功能,首先POST一筆TrialUser資料至WebApi中 (注意要加入Typename作為識別用)
進入產生物件實體邏輯中,順利生成正確TrialUser物件實體
WebApi接收之TrialUser物件實體資料正確無誤
參考資訊
http://stackoverflow.com/questions/12638741/deserialising-json-to-derived-types-in-asp-net-web-api
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !