應用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 就沒有此情況了) ^_^

Post a comment