The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

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 專案
  2. 新增一個 ADO.NET Entity Data Model 項目 (Entity Framework) 到 Models 目錄下,該資料模型會包含一些表格之間的關聯性。
  3. 新增一個 API 控制器
  4. 最後產出的程式碼如下:
  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]'.

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

而當 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)即可防止參考循環問題發生。程式碼範例如下:

   

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

 

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

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 種,明確列出哪些屬性不要輸出,這個方法在未來遇到的問題最小。

 

相關連結