EDMX Navigation Property 屬性引發 JSON 物件循環參考錯誤與解決辦法[转]
2013年4月
Entity Framework (EDMX) Navigation Property
當我們透過Model first或Database first建立Entity Framework實體資料模式(EDMX),例如下圖是一個ASP.NET Web API專案,從範例Northwind資料庫裡加入兩張有關連性的資料表到EDMX,資料表的關連性在EDMX裡會以Navigation Property來呈現。
當我們什麼都不修改,直接加入API Controller,例如,ProcutsController和OrderDetailsController然後建置、啟動、測試ASP.NET Web API服務會得到一個錯誤。
完整訊息如下:
{ "Message": "發生錯誤。", "ExceptionMessage": "'ObjectContent`1' 類型無法序列化內容類型 'application/json; charset=utf-8' 的回應主體。", "ExceptionType": "System.InvalidOperationException", "StackTrace": null, "InnerException": { "Message": "發生錯誤。", "ExceptionMessage": "Self referencing loop detected for property 'Product' with type 'System.Data.Entity.DynamicProxies.Product_65FAC6E44EE4BB6B00D5AD1D9A45D7BE6D877BB340CA7CD682A1F0D0A551EE53'. Path '[0].Order_Details[0]'.", "ExceptionType": "Newtonsoft.Json.JsonSerializationException", "StackTrace": " 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IWrappedCollection values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IWrappedCollection values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)\r\n 於 Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value)\r\n 於 Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value)\r\n 於 Newtonsoft.Json.JsonSerializer.Serialize(JsonWriter jsonWriter, Object value)\r\n 於 System.Net.Http.Formatting.JsonMediaTypeFormatter.<>c__DisplayClassd.<WriteToStreamAsync>b__c()\r\n 於 System.Threading.Tasks.TaskHelpers.RunSynchronously(Action action, CancellationToken token)" } }
從錯誤訊息(ExceptoinMessage)可以找到問題點,JSON.NET發現了一個「Self referencing loop」的問題。查詢Northwind.tt下關連的Product.vb與Order_Detail.vb:
Product.vb Partial Public Class Product ' …略 … ' 參考了Order_Detail Public Overridable Property Order_Details As ICollection(Of Order_Detail) = New HashSet(Of Order_Detail) End Class Order_Detail.vb Partial Public Class Order_Detail ' …略 … ' 參考了Product Public Overridable Property Product As Product End Class
這種「物件A參考物件B,物件B又參考物件A」的情況稱為物件循環參考。
JSON物件循環參考解決辦法
解決辦法一:使用WebApiConfig組態檔設置物件循環參考處理方法
在WebApiConfig組態檔中進行以下設置:
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
不過此種方式會副作用,就是產生的JSON格式內容可能和我們預期的內容有很大的差異,例如:
{ "$id": "1", "$values": [ { "$id": "2", "Order_Details": { "$id": "3", "$values": [ { "$id": "4", "Product": { "$ref": "2" }, "OrderID": 10285, "ProductID": 1, "UnitPrice": 14.4, "Quantity": 45, "Discount": 0.2 }, { "$id": "5", "Product": { "$ref": "2" }, "OrderID": 10294, "ProductID": 1, "UnitPrice": 14.4, "Quantity": 18, "Discount": 0 }, // …略…
解決辦法二:使用部分類別進行JSON.NET屬性設置
使用部分類別是最好且最正確的做法,因為不管是在ASP.NET MVC或ASP.NET WebAPI裡,在進行Models的驗證屬性設置時,都一定會使用到部分類別,所以由部分類別來集中處理管理與設置是較好的,而且產生JSON格式又是正確的。
Product.vb(部分類別) Imports System.ComponentModel.DataAnnotations Imports Newtonsoft.Json <MetadataType(GetType(ProductMD))> Partial Public Class Product End Class Public Class ProductMD Public Property ProductID As Integer ' …略… ' 必須引用Newtonsoft.Json <JsonIgnore()> Public Overridable Property Order_Details As ICollection(Of Order_Detail) = New HashSet(Of Order_Detail) End Class Order_Detail.vb(部分類別) Imports System.ComponentModel.DataAnnotations Imports Newtonsoft.Json <MetadataType(GetType(Order_DetailMD))> Partial Public Class Order_Detail End Class Public Class Order_DetailMD Public Property OrderID As Integer ' …略… ' 必須引用Newtonsoft.Json <JsonIgnore()> Public Overridable Property Product As Product End Class
只需要在部份類別裡去設置JSON.NET的<JsonIgnore>屬性,這樣當ASP.NET Web API在處理含有物件循環參考的類別時,就會自行排除物件循環參考的情況。
參考資料: