2011年10月26日 星期三

使用Entity Framework 將物件轉為JSON時遇到循環參考錯誤 1


在ASP.NET MVC的Controller中會經常使用到JSON,通常都是前後端的資料處理,

後端產生物件資料後再轉為JSON傳到前端做AJAX的處理,而後端也會接收前端所傳來的JSON資料,

而ASP.NET MVC的Controller有很多的傳回類別,其中一個有關JSON的就是「JsonResult」,

JsonResult類別

http://msdn.microsoft.com/zh-tw/library/system.web.mvc.jsonresult.aspx
表示用來將JSON格式之內容傳送至回應的類別。(繼承自抽象類別ActionResult)

如果要經由Json(object, JsonRequestBehavior.AllowGet)的方法去傳回JsonResult,結構簡單的物件是不會發生問題的,

例如:

public JsonResult Product(int? id)
{
    if (!id.HasValue)
    {
        Dictionary<string, string> jo = new Dictionary<string, string>();
        jo.Add("Msg", "請輸入產品ID編號.");
        return Json(jo, JsonRequestBehavior.AllowGet);
    }
    else
    {
        ProductService service = new ProductService();
        var product = service.Single(id.Value);
        var result = new 
        { 
            ID = product.ProductID, 
            Name = product.ProductName, 
            CategoryID = product.CategoryID 
        };
        return Json(result, JsonRequestBehavior.AllowGet);
    }
}

結果:

image

image

但如果我們所取得的物件資料不會再去做物件欄位的選擇,而是直接將整個物件放到Json(object)方法中,

而Model的資料模型是使用ASO.NET Entity Framework,各個資料表都有設定關連時,這個時候就很容易出現以下的錯誤:

image

是的,出現了循環參考的錯誤!怎解呢?


先來看看程式的部份:

可以與前面的程式做個比較,下面的程式裡,我們取出一個Product物件後就直接將物件給放到Json()方法中

public JsonResult Product(int? id)
{
  if (!id.HasValue)
  {
    Dictionary<string, string> jo = new Dictionary<string, string>();
    jo.Add("Msg", "請輸入產品ID編號.");
    return Json(jo, JsonRequestBehavior.AllowGet);
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    return Json(product, JsonRequestBehavior.AllowGet);
  }
}

程式看起來沒有問題,而使用ADO.NET Entity Framework所產生的實體模型也依據資料庫的內容而作了Model間的關聯,

Product會去關聯到Category,而Category也會關聯到Product,

所以當取得一個Product物件時,也會連同其關連的Category物件也取回,

而取得Category物件後,也會將Category所關連的Products物件也取回,如此一直循環下去,

以致於會產生循環參考(circular reference).

image

 

先來看看ASP.NET MVC中的JsonResult類別的原始碼:

JsonResult是去使用JavaScriptSerializer來完成JSON序列化的操作

namespace System.Web.Mvc {
    using System;
    using System.Text;
    using System.Web;
    using System.Web.Mvc.Resources;
    using System.Web.Script.Serialization;
 
    public class JsonResult : ActionResult {
 
        public JsonResult() {
            JsonRequestBehavior = JsonRequestBehavior.DenyGet;
        }
 
        public Encoding ContentEncoding {
            get;
            set;
        }
 
        public string ContentType {
            get;
            set;
        }
 
        public object Data {
            get;
            set;
        }
 
        public JsonRequestBehavior JsonRequestBehavior {
            get;
            set;
        }
 
        public override void ExecuteResult(ControllerContext context) {
            if (context == null) {
                throw new ArgumentNullException("context");
            }
            if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
                String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) {
                throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);
            }
 
            HttpResponseBase response = context.HttpContext.Response;
 
            if (!String.IsNullOrEmpty(ContentType)) {
                response.ContentType = ContentType;
            }
            else {
                response.ContentType = "application/json";
            }
            if (ContentEncoding != null) {
                response.ContentEncoding = ContentEncoding;
            }
            if (Data != null) {
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                response.Write(serializer.Serialize(Data));
            }
        }
    }
}

 

如果說要解決這個循環參考的問題,其實有個方法可以解決。哪就是使用[ScriptIgnore]

ScriptIgnoreAttribute 類別

http://msdn.microsoft.com/zh-tw/library/system.web.script.serialization.scriptignoreattribute.aspx

指定 JavaScriptSerializer 不會將公用屬性或公用欄位序列化。 此類別無法被繼承。

如果將 ScriptIgnoreAttribute 套用至某個類別的公用屬性或公用欄位,則將此類別的執行個體序列化至 JavaScript 物件標記法 (JSON) 格式時,JavaScriptSerializer 會忽略或跳過這個成員。

例如:

using System;
using System.Web.Script.Serialization;
public class Group
{
    // The JavaScriptSerializer ignores this field.
    [ScriptIgnore]
    public string Comment;
    // The JavaScriptSerializer serializes this field.
    public string GroupName;
}

但是會有個問題,如果我們是使用Model-First的EF,那資料庫的表格或是欄位有任何修改,則EF的模型(edmx)就會需要更新,

而Entities.Designer.cs的內容也就會被重新產生一次,之前在欄位或是屬性所加上去的[ScriptIgnore]就會不見,

每次的改動就要去手動去加上[ScriptIgnore]的Attribute,這麼做真的很累也很不切實際,

加上[ScriptIdnore]的作法比較適合在EF的Code-First上而不適合用在Model-First上。

 

那麼…還有什麼方法可以解決呢?

其實還有一個比較簡單的作法,那就是直接使用Json.NET這個套件,JSON.NET這套件也是我最常在專案用來操作JSON資料,

Json.NET

http://james.newtonking.com/pages/json-net.aspx

Json.NET CodePlex Project

Json.NET Download

NuGet Gallery : http://nuget.org/List/Packages/Newtonsoft.Json

 

使用Json.NET就不要使用JsonResult類別傳回,而是需要傳回類別須使用ActionResult

如果使用JsonResult類別傳回的話,就會有以下的狀況發生:

image

PS.有此問題發生的人,可以參考以下幾篇文章(無法解決循環參考不過可以解決JsonLength過長的問題):

MSDN -  jsonSerialization Element (ASP.NET Settings Schema)

Stackoverflow - Exceeding MaxJsonLength when Rendering View as a String

Thoughtful Code - Custom JsonResult Class for ASP.Net MVC to Avoid MaxJsonLength Exceeded Exception

 

如果是有取出指定物件欄位,然後再用JsonConvert.SerializeObject()去轉換,也使用JsonResult傳回的話…

public JsonResult Product(int? id)
{
    if (!id.HasValue)
    {
        Dictionary<string, string> jo = new Dictionary<string, string>();
        jo.Add("Msg", "請輸入產品ID編號.");
        return Json(jo, JsonRequestBehavior.AllowGet);
    }
    else
    {
        ProductService service = new ProductService();
        var product = service.Single(id.Value);
        var result = new
        {
            ID = product.ProductID,
            Name = product.ProductName,
            CategoryID = product.CategoryID
        };
        return Json(JsonConvert.SerializeObject(result), JsonRequestBehavior.AllowGet);
    }
}

傳回來的Content-Type是application/json沒錯(用JsonResult傳回就是預設傳回Content-Type為application/json,看上面的原始碼)

image

但是傳回的東西,看起來好像是JSON文件,但是卻無法正確解析…

image

因為格式不對,雖然傳回的Content-Type是「application/json」但就是無法解析出正確的JSON內容。

 

使用Json.NET以及回傳類別為ActionResult並指定Content-Type

修改後的程式:

public ActionResult Product(int? id)
{
  if (!id.HasValue)
  {
    Dictionary<string, string> jo = new Dictionary<string, string>();
    jo.Add("Msg", "請輸入產品ID編號.");
    return Content(JsonConvert.SerializeObject(jo), "application/json");
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    return Content(JsonConvert.SerializeObject(product), "application/json");
  }
}
執行結果:

image

Content-Type為指定的「application/json」而且可以正確解析出JSON內容

image

不過就只有一個Product物件而已,怎麼會從後端傳了一個「1.8MB」的資料呢?

 

其實答案很簡單,就是因為EF的關係,把有關連的資料也給取了出來,Category', Order_Details, Supplier

在Category這個欄位裡,還把有關聯到的Product資料也帶了出來… 其他像是Order_Details, Supplier也是一樣的情況,

這樣的資料不肥才怪!

image

 

簡單的解法就是,當我們要取Product物件時,轉為Json前先取出我們要的欄位資料,又或者說只取出Product本身的欄位,

程式改成下面的內容:

public ActionResult Product(int? id)
{
  if (!id.HasValue)
  {
    Dictionary<string, string> jo = new Dictionary<string, string>();
    jo.Add("Msg", "請輸入產品ID編號.");
    return Content(JsonConvert.SerializeObject(jo), "application/json");
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    var result = new
    {
      ProductID = product.ProductID,
      ProductName = product.ProductName,
      SupplierID = product.SupplierID,
      CategoryID = product.CategoryID,
      CategoryName = product.Category.CategoryName,
      QuantityPerUnit = product.QuantityPerUnit,
      UnitPrice = product.UnitPrice,
      UnitsInStock = product.UnitsInStock,
      UnitsOnOrder = product.UnitsOnOrder,
      ReorderLevel = product.ReorderLevel
    };
    return Content(JsonConvert.SerializeObject(result), "application/json");
  }
}

而執行的結果:Json的資料大小就減少到只有205 B而已

image

但是這樣的作法卻挺麻煩的,物件的欄位少還無所謂,一旦碰上了欄位很多的物件時,這樣手動指定物件欄位的作法就不切實際。

 

嗯…這篇寫得有點長了,所以下一篇再繼續…

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。