asp.net mvc 的webApi (二)vs自动生成的问题

来源:互联网 发布:金秀贤是否过气了 知乎 编辑:程序博客网 时间:2024/05/16 19:28


1.在新建一个webApi 的控制器的时候,选择:自动生成EF的API接口,



生成了很多接口。

但是会出现异常:

{"Message":"发生错误。","ExceptionMessage":"“ObjectContent`1”类型未能序列化内容类型“application/json; charset=utf-8”的响应正文。","ExceptionType":"System.InvalidOperationException","StackTrace":null,"InnerException":{"Message":"发生错误。","ExceptionMessage":"Self referencing loop detected with type 'System.Data.Entity.DynamicProxies.BlogArticle_15878A5E36B73ACBB2A51C5F93BAD0476377EAA00186ACA054C0D4B7B8B72296'.

1.在 Global.asax.cs 中的 Application_Start() 加上以下程式,宣告 Web API 自動忽略所有參考循環的處理

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

2.处理导航属性,也就是引用参数;

3.修改路由配置

解决方案:

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



另外一个文献:

一般我们在开法 ASP.NET Web API 时,如果是使用 Entity Framework 技术来操作数据库的话,当两个 Entity 之间包含导览属性(Navigation Property)时,而当我们输出的格式为 JSON 对象时,会出现一个例外,错误讯息为:「'ObjectContent`1' 类型无法序列化内容类型 'application/json; charset=utf-8' 的回应主体。」,而小弟参考了 Will 保哥以及 Bruce 两位前辈的文章后,整理出两种小弟觉得比较可行的替代与解决方案。

了解问题

 

\
 

这张图里包含了两张数据表 Orders 与 Order_Details ,两者之间存在着一对多的关系,而预设 Entity Framework 会自动帮我们的关联数据表加入导览属性(Navigation Property),接着我们往下一张图看下去:

        public IEnumerable<Orders> GetOrders()
        {
            return db.Orders;
        }

这段程序代码为 ValuesController 里的一个 Function ,当我们请求时会返回 Orders 所有数据,但当我们输入网址 /api/Values/ 请求时却发生了这样的错误:

 \
 

这个问题发生的原因为,当我们请求某个特定的 Enity 时会取出该 Entity 的所有属性内容,当然包夸了导览属性的数据,而究竟这个问题如何照成呢?以目前的案例来看,当我们取得 Orders 的资料时也会一并取得其导览属性,也就是 Order_Details 的所有数据,而在 Order_Details 里也包含着 Orders 的导览属性,所以又会在去取得 Orders 的数据....,这样两个实体之间不停的往返就会造成了无限循环,也是我们前面所说的循环参考的错误。

如何解决


namespace MvcApplication3.Models
{
    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;


    [MetadataType(typeof(BlogUserMetadata))]
    public partial class BlogUser
    {
        public BlogUser()
        {
            this.BlogArticle = new HashSet<BlogArticle>();
            this.BlogArticleCate = new HashSet<BlogArticleCate>();
        }
    
        public int Id { get; set; }
        public string LoginName { get; set; }
        public string LoginPwd { get; set; }
        public string CnName { get; set; }
        public string Email { get; set; }
        public int IsLock { get; set; }
        public bool IsDel { get; set; }
        public System.DateTime AddTime { get; set; }
        public System.DateTime LastLoginTime { get; set; }
        public virtual ICollection<BlogArticle> BlogArticle { get; set; }
        public virtual ICollection<BlogArticleCate> BlogArticleCate { get; set; }
       
        private class BlogUserMetadata
        {
            [JsonIgnore]
            public virtual ICollection<BlogArticle> BlogArticle { get; set; }
            [JsonIgnore]
            public virtual ICollection<BlogArticleCate> BlogArticleCate { get; set; }
        }
        
    }
}

方法一:

在开发 ASP.NET MVC 中,时常会用到部分类别(Partail Class)来为我们的数据域位加上验证属性,所以利用此特性来解决我们的问题,首先在 Model 数据夹底下新增两个档案分别为:OrdersMetadata.cs 、Order_DetailsMetadata.cs

view sourceprint?
01.OrdersMetadata.cs
02. 
03.[MetadataType(typeof(OrderMD))]
04.publicpartialclassOrder
05.{
06.publicclassOrderMD
07.{
08.[JsonIgnore()]// 需引用 using Newtonsoft.Json;
09.publicvirtualICollection<Order_details> Order_Details {get;set; }
10.}
11.}




12. Order_DetailsMetadata.cs

13.[MetadataType(typeof(Order_DetailsMD))]
14.publicpartialclassOrder_Details
15.{
16.publicclassOrder_DetailsMD
17.{
18.[JsonIgnore()] // 需引用 using Newtonsoft.Json;
19.publicvirtualOrders Orders {get;set; }
20.}
21.}

这边我们在在对应的导览属性上都加上 「JsonIgnore」的属性,来防止循环参考的问题发生,记得是有关联的两边都要加上「JsonIgnore」的属性。

方法二:

另外一种方法则是利用我们在开发 ASP.NET MVC 时常用到的 ViewModel 的概念,针对特定的页面或请求只返回特定的数据,所以这边我们能透过 DTO 方式来解决我们问题,首先我们先在 Model 底下新增一个 DTO.cs 档案: www.it165.net

DTO.cs

view sourceprint?
01.publicclassOrderDTO
02.{
03.publicintOrderID {get;set; }
04.publicstringCustomerID {get;set; }
05.publicint? EmployeeID {get;set; }
06.publicDateTime? OrderDate {get;set; }
07.publicList<Order_detailsDTO> Order_Detail {get;set; }
08.}
09. 
10.publicclassOrder_DetailsDTO
11.{
12.publicintOrderID {get;set; }
13.publicdecimalUnitPrice {get;set; }
14.publicdecimalQuantity {get;set; }
15.}

并且修改我们原本在 Web API Controller 里的 Function:

view sourceprint?
01.publicIEnumerable<OrderDTO> GetOrders()
02.{
03.NorthwindEntities db =newNorthwindEntities();
04. 
05.returndb.Orders.ToList().Select(p =>newOrderDTO
06.{
07.CustomerID = p.CustomerID,
08.EmployeeID = p.EmployeeID,
09.OrderDate = p.OrderDate,
10.OrderID = p.OrderID,
11.Order_Detail = p.Order_Details.Select(x =>newOrder_DetailsDTO
12.{
13.OrderID = x.OrderID,
14.Quantity = x.Quantity,
15.UnitPrice = x.UnitPrice
16.}).ToList()
17.}); ;
18.}

总结

两种作法都能解决造成循环对象参考的问题,而在 Bruce 和 Will 保哥的文章都指出用第一种方法来解决此循环参考错误是最正确的作法,两种作法算是两种不同的出发点,所以该怎么用应该就是看各位读者如何应用了。


0 0
原创粉丝点击