[料理佳餚] ASP.NET MVC 自訂 ModelBinder 將宣告為抽象型別的參數反序列化

如果我們是真的用物件導向在設計程式,那麼一定會用到抽象類的型別(Abstract Class、Interface),在現今當下的資料交換格式中,JSON 算是大家首選的格式,可是當我們的設計相依於抽象之後,序列化及反序列化就變成一個我們必須特別要處理的點,序列化倒是還好,反序列化就比較頭痛了。

假設我有一個資料模型的設計是這樣子的,Customer 是抽象類別,以客戶的所屬地區來產生 Customer 的派生類別,在 Customer 中定義了一個 List<Order> 是記錄客戶下的訂單,而 Order 本身也是個抽象類別,以產品類別來產生 Order 的派生類別。

我設計了一個 CustomerController,其中有一個 Add(Customer customer) 的 Action,我們可以看到 Add() 的參數是 Customer,因此 Add() 相依於 Customer 這個抽象類別。

而對於呼叫 Add() 的 Client 端來說,它只要面對 Add(Customer customer) 這個 Api 方法,不必管後端是怎麼處理來自不同地區的 Customer,即使需求要加入一個來自 America 的客戶,Client 端面對的依舊是 Add(Customer customer),不會因為加了來自 America 的客戶,Client 端就需要多呼叫 Add(AmericaCustomer americaCustomer) Api 方法。

自訂 Customer ModelBinder

但是如果我們不做處理直接就這樣上場,在 Client 端呼叫 Add() 時,反序例化就會出問題。

這時候我們就必須自己做 ModelBinder,建立 CustomerModelBinder 實作 IModelBinder 自己來處理反序列化,而我反序列化的工具是用 Json.NET

public class CustomerModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);

        var stream = new StreamReader(controllerContext.RequestContext.HttpContext.Request.InputStream, Encoding.UTF8);

        var json = stream.ReadToEnd();

        return JsonConvert.DeserializeObject(json, bindingContext.ModelType);
    }
}

接著在要使用自定義 ModelBinder 的 Action 參數前加上這一串。

自訂 Customer JsonConverter

我們直接用 JsonConvert.DeserializeObject 將收到的內容直接反序列化,以為這樣就可以了,殊不知我們還差一步。

JsonConvert 不知道我們實際上要反序列化的派生類別是什麼,這個部分我們就必須自己自訂 JsonConverter 來告訴 JsonConvert 我們優先要使用的 JsonConverter 是哪些?

在這個範例裡面,要反序列化的對象有 2 個抽象類別 CustomerOrder 所以我們必須要針對這兩個抽象類別各自做一個 JsonConverter 來對應。

CustomerConverter

public class CustomerConverter : JsonConverter
{
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return
            objectType.Equals(typeof(List<Customer>))
            || objectType.Equals(typeof(Customer));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType.Equals(JsonToken.StartArray))
        {
            return
                JArray.Load(reader)
                      .Cast<JObject>()
                      .Select(o =>
                      {
                          return GenerateCustomerObject(o, serializer);
                      })
                      .ToList();
        }

        if (reader.TokenType.Equals(JsonToken.StartObject))
        {
            return GenerateCustomerObject(JObject.Load(reader), serializer);
        }

        return null;
    }

    private Customer GenerateCustomerObject(JObject jobj, JsonSerializer serializer)
    {
        switch ((CustomerType)(int)jobj["Type"])
        {
            case CustomerType.Taiwan: return jobj.ToObject<TaiwanCustomer>(serializer);
            case CustomerType.America: return jobj.ToObject<AmericaCustomer>(serializer);
            default: return null;
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

OrderConverter

public class OrderConverter : JsonConverter
{
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return
            objectType.Equals(typeof(List<Order>))
            || objectType.Equals(typeof(Order));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType.Equals(JsonToken.StartArray))
        {
            return
                JArray.Load(reader)
                      .Cast<JObject>()
                      .Select(o =>
                      {
                          return GenerateOrderObject(o, serializer);
                      })
                      .ToList();
        }

        if (reader.TokenType.Equals(JsonToken.StartObject))
        {
            return GenerateOrderObject(JObject.Load(reader), serializer);
        }

        return null;
    }

    private Order GenerateOrderObject(JObject jobj, JsonSerializer serializer)
    {
        switch ((OrderType)(int)jobj["Type"])
        {
            case OrderType.Book: return jobj.ToObject<BookOrder>(serializer);
            case OrderType.Car: return jobj.ToObject<CarOrder>(serializer);
            default: return null;
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

最後在呼叫 JsonConvert.DeserializeObject 的時候,明確地指定我們要使用的 JsonConverter。

加上去之後,JsonConvert 就可以幫我們做明確地轉型了。

最後提醒一下,我們在做抽象化設計的時候,最好能設計一個列舉型別進去,讓處理序列化結果的 Client 端,可以明確地知道目前收到的序列化物件實際的型別是什麼,以方便 Client 端做精準地判斷。

參考資料

 < Source Code >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學