ASP.NET Web API - Entity Framework(EDMX) Navigation Property引發的JSON物件循環參考錯誤

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
   

書上因為範例簡單,所以當初沒測試到這問題,麻煩各位修正。如果書有幸二刷,正確的參數會提供給書商進行修正之。但部分類別是新增內容,如果有二版的話,會放進去。^^

沒有留言:

張貼留言

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