Entity Framework Model物件之Json.NET還原問題
1 |
應用ASP.NET MVC時,透過ActionResult傳回Entity Framework資料物件的JSON格式,接收端試著用Json.NET解析卻發生錯誤! 研究發現,在ASP.NET MVC Action中以return JSON(someObject)方式傳回JSON字串時,會使用JavaScriptSerializer進行序列化。換句話說,問題出在"使用JavaScriptSerializer序列化Entity Framework資料物件,再以Json.NET反序列化還原",以下用範例程式重現問題。
借用EF View Primary Key錯置問題一文的Team類別,程式由資料庫查詢一筆資料,分別使用JavaScriptSerializer及Json.NET序列化,再用JavaScriptSerializer還原Json.NET產生的JSON字串,用Json.NET還原JavaScriptSerializer產生的JSON字串,進行交叉測試。
static void Main(string[] args)
{
using (var ctx = new LabEntities())
{
//由資料庫取得Team物件
var team = ctx.Team.Single(o => o.TeamName == "Avengers");
//使用JavaScriptSerializer序列化
JavaScriptSerializer jss = new JavaScriptSerializer();
string json1 = jss.Serialize(team);
Console.WriteLine("JavaScriptSerializer: {0}", json1);
//使用Json.NET序列化
string json2 = JsonConvert.SerializeObject(team);
Console.WriteLine("Json.NET: {0}", json2);
//使用JavaScriptSerializer還原Json.NET序列化的結果
try
{
var t = jss.Deserialize<Team>(json2);
Console.WriteLine("JavaScriptSerializer: {0}", t.TeamName);
}
catch (Exception ex)
{
Console.WriteLine("JavaScriptSerializer Error: {0}", ex.Message);
}
//使用Json.NET還原JavaScriptSerializer序列化的結果
try
{
var t = JsonConvert.DeserializeObject<Team>(json1);
Console.WriteLine("Json.NET: {0}", t.TeamName);
}
catch (Exception ex)
{
Console.WriteLine("Json.NET Error: {0}", ex.Message);
}
}
Console.Read();
}
執行結果如下:
JavaScriptSerializer: {"TeamId":"T003","TeamName":"Avengers","Country":"USA","En tityState":2,"EntityKey":{"EntitySetName":"Team","EntityContainerName":"LabEntit ies","EntityKeyValues":[{"Key":"TeamId","Value":"T003"}],"IsTemporary":false}}
Json.NET: {"$id":"1","TeamId":"T003","TeamName":"Avengers","Country":"USA","Enti tyKey":{"$id":"2","EntitySetName":"Team","EntityContainerName":"LabEntities","En tityKeyValues":[{"Key":"TeamId","Type":"System.String","Value":"T003"}]}}
JavaScriptSerializer: Avengers
Json.NET Error: Expected JSON property 'Type'.
由JSON字串發現Team除了我們所認知的TeamId、TeamName及Country屬性外,還有EntityState、EntityKey兩個額外EF專用屬性,而由資料庫取出的Team物件EntityState/EntityKey會有內容(註: new Team()產生尚未加入資料庫的則無值),而Json.NET針對EntityKey做了額外處理,序列化後多產生了$id屬性。(在.NET Class中本無此屬性)
JavaScriptSerializer可以還原Json.NET所產生多出"$id"的JSON字串;但Json.NET在還原JavaScriptSerializer所產生JSON字串時,卻冒出Expected JSON property 'Type'.錯誤。
推測Json.NET在還原具有EntityKey屬性的JSON字串時,預期應有$id等自己客製的額外屬性,一旦落空就發生問題。為了印證此點,我將JavaScriptSerializer產生的JSON字串,先還原成動態JObject物件,移去EntityKey欄位後再重新產生JSON字串(其中不含EntityKey),修正後的JSON字中使用Json.NET還原便能正確解析成Team物件。
static void Main(string[] args)
{
using (var ctx = new LabEntities())
{
//由資料庫取得Team物件
var team = ctx.Team.Single(o => o.TeamName == "Avengers");
//使用JavaScriptSerializer序列化
JavaScriptSerializer jss = new JavaScriptSerializer();
string json = jss.Serialize(team);
Console.WriteLine("JavaScriptSerializer: {0}", json);
//試著拆解掉EntityKey
JObject jo = JsonConvert.DeserializeObject<JObject>(json);
jo.Remove("EntityKey");
//產生修正版JSON
string fixedJson = jo.ToString();
Console.WriteLine("Fixed JSONr: {0}", fixedJson);
//使用Json.NET還原修正版JSON
var t = JsonConvert.DeserializeObject<Team>(fixedJson);
Console.WriteLine("Json.NET: {0}", t.TeamName);
}
Console.Read();
}
以下是執行結果:
JavaScriptSerializer: {"TeamId":"T003","TeamName":"Avengers","Country":"USA","En
tityState":2,"EntityKey": "EntitySetName":"Team","EntityContainerName":"LabEntities","EntityKeyValues":[{"Key":"TeamId","Value":"T003"}],"IsTemporary":false}}
Fixed JSONr: {
"TeamId": "T003",
"TeamName": "Avengers",
"Country": "USA",
"EntityState": 2
}
Json.NET: Avengers
先解成JObject,移除EntityKey後重新產生JSON再反序列化感覺有點笨拙,爬文找到更優雅的解法,利用Json.NET強大的擴充性,實做一個ExcludeEntityKeyContractResolver,便可指定Json.NET在反序列化時忽略EntityKey,一次到位:
static void Main(string[] args)
{
using (var ctx = new LabEntities())
{
//由資料庫取得Team物件
var team = ctx.Team.Single(o => o.TeamName == "Avengers");
//使用JavaScriptSerializer序列化
JavaScriptSerializer jss = new JavaScriptSerializer();
string json = jss.Serialize(team);
Console.WriteLine("JavaScriptSerializer: {0}", json);
//加入客製ContractResolver,還原時無視EntityKey
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new ExcludeEntityKeyContractResolver();
//使用Json.NET還原JavaScriptSerializer序列化的結果
var t = JsonConvert.DeserializeObject<Team>(json, settings);
Console.WriteLine("Json.NET: {0}", t.TeamName);
}
Console.Read();
}
public class ExcludeEntityKeyContractResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(
Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> properties =
base.CreateProperties(type, memberSerialization);
return properties.Where(p =>
p.PropertyType != typeof(System.Data.EntityKey)).ToList();
}
}
測試成功!
JavaScriptSerializer: {"TeamId":"T003","TeamName":"Avengers","Country":"USA","EntityState":2,"EntityKey":{"EntitySetName":"Team","EntityContainerName":"LabEntities","EntityKeyValues":[{"Key":"TeamId","Value":"T003"}],"IsTemporary":false}}
Json.NET: Avengers
Comments
# by KKBruce
補充一下,在目前 MVC 4 RC 中,ActionResult型別裡的 Json 已經改由 JSON.NET 來實作,未來的 MVC 4 專案中應該不會再出現此情況。(其實 MVC 4 RC 就沒有此情況了) ^_^