使用jQuery傳送物件JSON到ASP.NET MVC的做法之前介紹過,但最近我在專案又遇到新難題。
例如有一個參數物件,ArgObject,內含Name屬性及SubArg屬性,SubArg有其專屬型別SubArgObject,基於特殊需要,SubArgObject使用[JsonProperty]及[JsonIgnore]自訂JSON轉換邏輯(實際專案用的是[JsonConverter(...)],此處簡化為[JsonProperty],指定PropB在JSON中需更名為PropX):
publicclass ArgObject
{
publicstring Name { get; set; }
public SubArgObject SubArg { get; set; }
}
publicclass SubArgObject
{
publicstring PropA { get; set; }
[JsonProperty("PropX")]
publicstring PropB { get; set; }
[JsonIgnore]
publicstring PropC { get; set; }
}
如果是Web API Controller,什麼都不用做就能順利接收參數。依Web API的Binding規則,參數如為複雜型別(Complex Type),將依Content-Type選擇適當的Media Type Formatter進行轉換[參考]。在本例中application/json會使用JsonMediaTypeFormatter,其核心為Json.NET,且已知轉換目標型別為ArgObject,故可精準轉換[JsonProperty("PropX")]無誤。
publicclass WebApiController : ApiController
{
public ArgObject PostJson(ArgObject args)
{
return args;
}
}
同樣參數與POST內容,套用在MVC Action結果截然不同:(補充:傳回結果採用Json.NET版JsonResult,使用Json.NET序列化,避用預設的JavaScriptSerializer)
publicclass MvcController : Controller
{
public ActionResult PostJson(ArgObject args)
{
returnnew Newtonsoft.JsonResult.JsonResult()
{
Data = args
};
}
}
JSON中的PropX值沒有正確對應到PropB:
這個差異源自ApiController及Controller處理Model Binding的機制不同。Controller使用Value Provider概念解析Query Sting參數、HTML Form欄位、XML或JSON,再對應成輸入參數,而JSON由JsonValueProviderFactory負責解析,其內部使用的是JavaScriptSerializer。網路上有將JsonValueProviderFactory換成Json.NET版JsonDotNetValueProviderFactory的做法,但實測無法克服問題。因為MVC預設的Model Binding機制,會先用JsonValuProviderFactory將JSON內容轉成包含三組值的Dictionary:
Name: "Jeffrey",
SubArg.PropA: "AAA",
SubArg.PropX: "XXX"
再從Dictionary取值映對到新建的ArgObject及SubArgObject物件,導致[JsonProperty("PropX")]無從發揮作用。
評估之下,還是得回歸自訂Model Binder。執行IModelBinder.BindModel()時,因轉換對象型別已知,可善用Json.NET的.DeserializeObject(jsonString, bindingContext.ModelType),指定目標型別讓[JsonProperty]、[JsonConverter(…)]等設定發揮作用。
仿效System.Web.Http.FromBodyAtrribute,我做了一個Afet.Mvc.FromBodyAtrribute,將整個JSON內容轉成單一型別。
另外,藉此機會也順便解決實務上另一項常見困擾。為了傳送JSON到SomeAction(Type1 arg1, Type2 arg2),通常要另外宣告一個暫用彙總型別:
class AggTypes {
public Type1 arg1 { get; set; }
public Type2 arg2 { get; set; }
}
接收端再改成SomeAction([Afet.Mvc.FromBody]AggTypes args)。既有自訂Model Binder,若允許由JSON中只取出arg1及arg2,分別轉成Type1及Type2,就可以省去多宣告無意義彙總型別的困擾:
ActionResult SomeAction([Afet.Mvc.FromPartialBody]Type1 arg1, [Afet.Mvc.FromPartialBody]Type2 arg2)
FromBodyAttribute及FromPartialBodyAttribute的程式碼如下:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http.Controllers;
using System.Web.Http.Validation;
using System.Web.Mvc;
namespace Afet.Mvc
{
[AttributeUsage(AttributeTargets.Parameter)]
publicclass FromBodyAttribute : CustomModelBinderAttribute
{
protectedbool DictionaryMode = false;
public FromBodyAttribute(bool dictionaryMode = false)
{
this.DictionaryMode = dictionaryMode;
}
publicoverride IModelBinder GetBinder()
{
returnnew JsonNetBinder(DictionaryMode);
}
publicclass JsonNetBinder : IModelBinder
{
privatebool DictionaryMode;
public JsonNetBinder(bool dictionaryMode)
{
this.DictionaryMode = dictionaryMode;
}
conststring VIEW_DATA_STORE_KEY = "___BodyJsonString";
publicobject BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
//Verify content-type
if (!controllerContext.HttpContext.Request.
ContentType.ToLower().Contains("json"))
returnnull;
var stringified = controllerContext.Controller
.ViewData[VIEW_DATA_STORE_KEY] asstring;
if (stringified == null)
{
var stream = controllerContext.HttpContext.Request.InputStream;
using (var sr = new StreamReader(stream))
{
stream.Position = 0;
stringified = sr.ReadToEnd();
controllerContext.Controller
.ViewData.Add(VIEW_DATA_STORE_KEY, stringified);
}
}
if (string.IsNullOrEmpty(stringified)) returnnull;
try
{
if (DictionaryMode)
{
//Find the property form JObject converted from body
var dict = JsonConvert.DeserializeObject<JObject>(stringified);
if (dict.Property(bindingContext.ModelName) == null)
returnnull;
//Convert the property to ModelType
return dict.Property(bindingContext.ModelName).Value
.ToObject(bindingContext.ModelType);
}
else
{
//Convert the whole body to ModelType
return JsonConvert.DeserializeObject(stringified,
bindingContext.ModelType);
}
}
catch
{
}
returnnull;
}
}
}
[AttributeUsage(AttributeTargets.Parameter)]
publicsealedclass FromPartialBodyAttribute : FromBodyAttribute
{
public FromPartialBodyAttribute()
: base(true)
{
}
}
}
使用方法如下:
public ActionResult PostJson2(
[Afet.Mvc.FromBody]ArgObject args)
{
returnnew Newtonsoft.JsonResult.JsonResult()
{
Data = args
};
}
public ActionResult PostJson3(
[Afet.Mvc.FromPartialBody]string Name,
[Afet.Mvc.FromPartialBody]SubArgObject SubArg
)
{
returnnew Newtonsoft.JsonResult.JsonResult()
{
Data = new
{
ParamName = Name,
ParamSubArg = SubArg
}
};
}
有了這兩項工具,使用Json.NET解析MVC輸入參數就方便多了。