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


沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。