﻿# ASP.NET Web API 無法輸出 Entity Framework 物件的解法

開發 ASP.NET Web API 時，如果專案使用 Entity Framework 技術的話，當 Entity 與 Entity 之間包含導覽屬性 (Navigation Property) 的話，在預設的情況下，ASP.NET Web API 在輸出 JSON 格式時，會引發一個 System.InvalidOperationException 的例外狀況，其錯誤訊息為「'ObjectContent\`1' 類型無法序列化內容類型 'application/json; charset=utf-8' 的回應主體。」，若要解決這個問題，有幾個必須注意的地方，才能讓 ASP.NET Web API 正常且穩定的運作。

首先，我們先來看看這個錯誤發生的過程。

1.  建立一個 ASP.NET MVC 4 的 Web API 專案  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/0bf00cfda1e28d84c2b6-SNAGHTMLc439a9.png)
2.  新增一個 **ADO.NET Entity Data Model** 項目 (Entity Framework) 到 Models 目錄下，該資料模型會包含一些表格之間的關聯性。  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/4062aed3766b27ab5f7d-image_1717.png)
3.  新增一個 API 控制器  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/79e5a6ef3f312341cb9a-SNAGHTMLc38143.png)
4.  最後產出的程式碼如下：  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/1d05c997e86627c83aae-image_1718.png)
5.  為了確保透過瀏覽器執行 Web API 測試時一定會回應 **JSON** 格式，所以我在 Global.asax.cs 中的 Application\_Start() 加上以下程式：
    
    ```
    GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
    ```
    
6.  最後，執行這個 Web API 網站，就會看到一個 System.InvalidOperationException 的例外狀況，其錯誤訊息為「'ObjectContent\`1' 類型無法序列化內容類型 'application/json; charset=utf-8' 的回應主體。」，如果進一步查看 InnerException 的話，還會進一步看到以下錯誤訊息：「**Self referencing loop detected for property '產品類別'** with type 'System.Data.Entity.DynamicProxies.產品類別\_F665086487BD4B486CE39F9EE7A7428DD0F79AB0B98179E44C864A905E8A935D'. Path '\[0\].產品資料\[0\]'.」

發生這個問題的主因在於，「產品類別」與「產品資料」之間各有一個「導覽屬性」，如下圖示：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/b316dd1a849f312d1032-image_1719.png)

而當 ASP.NET Web API 在輸出特定一個 Entity 資料時，預設會取出該 Entity 上的所有屬性的內容，當然也包括「導覽屬性」的內容，也就是他會自動取得所有關聯資料。然而當我們從「產品類別」讀取「產品資料」這個導覽屬性時，便會取得所有該「產品類別」下的所有「產品資料」，而在「產品資料」裡，卻又有一個導覽屬性為「產品類別」參考到「產品類別」，也因此發生了 **參考循環** (**Reference Loop**) 的狀況，因而引發這個錯誤。

要解決這個問題，基本上**有 4 種解決方法**，優缺點都有，所以當你看完本篇文章之後，應該要思考到底哪種解法適合你的專案：

**1\. 直接從 Entity Framework 模型 (EDMX) 移除不必要的關聯屬性，以避免發生參考循環的狀況。**

-   優點：簡單、容易理解，針對需要快速開發 Web API 的專案可以這樣設定。
-   缺點：當要從「產品資料」關聯回「產品類別」時，就沒有可參考的導覽屬性可用。  
    註：若 Web API 真的只是為了提供資料給用戶端，其實可以直接從產品類別取得所有資料即可。

**2\. 開啟 Entity Framework 產生的 C# 類別定義檔 (ObjectContext 或 DBContext 或 Code First)，在特定導覽屬性上套用 \[JsonIgnore\] 屬性(Attribute)即可防止參考循環問題發生。**程式碼範例如下：

    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/4325828615e5f6149b83-image_1720.png)

-   優點：不用異動 Entity Framework 模型 (EDMX) 的定義，僅套用 \[JsonIgnore\] 屬性即可，有更好的關注點分離特性。
-   缺點：只要從 Visual Studio 異動 EDMX 內容，自訂修改後的程式碼都會 Visual Studio 的程式碼產生器覆蓋你之前的變更，除非你用 Code First 開發模式才沒有此問題。

**3\. 改進上述第 2 點的缺點，就是用 Partial Class 與 MetadataType 的方式擴充這些由 Visual Studio 幫我們產生的類別**，程式碼範例如下：

```csharp
namespace WebApi.Models
{
    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;

    [MetadataType(typeof(產品資料Metadata))]
    public partial class 產品資料
    {
        private class 產品資料Metadata
        {
            [JsonIgnore]
            public virtual 供應商 供應商 { get; set; }

            [JsonIgnore]
            public virtual ICollection<訂貨明細> 訂貨明細 { get; set; }

            [JsonIgnore]
            public virtual 產品類別 產品類別 { get; set; }
        }
    }
}
```

**4\. 在 Global.asax.cs 中的 Application\_Start() 加上以下程式，宣告 Web API 自動忽略所有參考循環的處理**：

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;

**注意**：這裡並不是真的「忽略」參考循環的問題，而是檢查到問題後不會引發例外，而且會自動隱藏輸出第二次參考回原本 Entity 的那個導覽屬性。

-   優點：簡單，針對需要快速開發 Web API 且資料量不大的專案可以這樣設定。
-   缺點：當資料量過大，參考循環又多時，伺服器端可能會引發 Out of Memory 的例外狀況，因為他會試圖把所有要輸出到用戶端的資料都讀入記憶體再轉成 JSON 格式。

**結論**

基於上述幾點解決方法，我認為最正規的解法應該是第 3 種，明確列出哪些屬性不要輸出，這個方法在未來遇到的問題最小。

**相關連結**

-   [JsonIgnore attributes not working in ASP.NET?](http://stackoverflow.com/questions/2954050/jsonignore-attributes-not-working-in-asp-net)
-   [failed to serialize the response in Web API](http://stackoverflow.com/questions/12641386/failed-to-serialize-the-response-in-web-api#_=_)
-   [如何讓 ASP.NET Web API 無論任何要求都回應 JSON 格式](/post/2012/10/12/ASPNET-Web-API-Force-return-JSON-format-instead-of-XML-for-Google-Chrome-Firefox-Safari.aspx)
