經測試作者方式, 在 DB Frist 下, 僅 從 edmx 內移除循環的導覽參考可正常Json輸出, 其他方式或許是必須在 MVC架構下才能work, 尚未實際測試過.
開發 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 正常且穩定的運作。
首先,我們先來看看這個錯誤發生的過程。
- 建立一個 ASP.NET MVC 4 的 Web API 專案
- 新增一個 ADO.NET Entity Data Model 項目 (Entity Framework) 到 Models 目錄下,該資料模型會包含一些表格之間的關聯性。
- 新增一個 API 控制器
- 最後產出的程式碼如下:
- 為了確保透過瀏覽器執行 Web API 測試時一定會回應 JSON 格式,所以我在 Global.asax.cs 中的 Application_Start() 加上以下程式:
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
- 最後,執行這個 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 種,明確列出哪些屬性不要輸出,這個方法在未來遇到的問題最小。
from http://blog.miniasp.com/post/2012/12/24/ASPNET-Web-API-Self-referencing-...