網頁

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

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

 

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

 

以上

沒有留言:

張貼留言