[Web API] 如何在WebAPI中正確解析(反序列化)抽象類別之子類別實體

如何在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中

 

image

 

結果可想而知為Null,因為JsonSerializer根本不知道要生成哪個子類別實體來轉換所接收到的JSON物件

 

image

 

那要如何來實現這個功能呢? 以下將歸納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結構中多了型別的資訊

 

image

 

也就因為此特性,所以我們可以在POST資料時明確指出類別的名稱,讓後端收到資料時建立相對應之類別實體,進而達到我們所希望的效果。測試如下,POST一筆TrialUser資料至WebApi中。

 

image

 

確實建立TrialUser實體且資料正確無誤

 

image

 

再測試一筆,這次POST一筆VipUser資料至WebApi中

 

image

 

這次也確實建立VipUser實體且資料正確無誤

 

image

 

此方式確實可以快速地達到我們的需求,但是大家應該也發現了該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來進行轉換。

 

image

 

[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作為識別用)

 

image

 

進入產生物件實體邏輯中,順利生成正確TrialUser物件實體

 

image

 

WebApi接收之TrialUser物件實體資料正確無誤

 

image

 

 

參考資訊

 

http://stackoverflow.com/questions/20408771/json-net-abstract-derived-class-deserialization-with-webapi-2

http://stackoverflow.com/questions/12638741/deserialising-json-to-derived-types-in-asp-net-web-api


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !