Entity Framework(EDMX) Navigation Property
上圖是一個ASP.NET Web API專案,從範例Northwind資料庫裡加入兩張有關連性的資料表到Entity Framework(EDMX)裡,很正常的,它會加入巡覽屬性(Navigation Property),當我們什麼都不修改,直接加入API Controller,例如,ProcutsController和OrderDetailsController然後建置、啟動、測試Web API服務會立即得到一個錯誤。
JSON的錯誤訊息與產生原因
完整訊息如下
{ "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)" } }
其實,錯誤訊息說的很明白了,我們的資料庫設計沒錯,有問題的是產生的資料庫類別。查詢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
這種你參考我,我參考你,就是物件循環參考。
JSON物件循環參考解決辦法
在Will的文章中,提供了四種方式,在《ASP.NET MVC 4網站開發美學》裡第7-143頁都有提到解決辦法,不過書中的設定參數是有問題的,也請讀者進行修改書中內容。
在書中我下的參數是:
' 書中設定參數, BAD ' config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.All ' Will設定參數, Good 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很明顯是有問題的。
處理物件循環參考最好作法
我和Will的意見是一致的,就是使用部份類別的作法,因為驗證方面,不太可能不使用部分類別去進行驗證屬性的設定,都一定會使用部分類別了,那最好的做法還是由部分類別去集中管理相關屬性設置。
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
沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。