在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);
}
}
結果:
但如果我們所取得的物件資料不會再去做物件欄位的選擇,而是直接將整個物件放到Json(object)方法中,
而Model的資料模型是使用ASO.NET Entity Framework,各個資料表都有設定關連時,這個時候就很容易出現以下的錯誤:
是的,出現了循環參考的錯誤!怎解呢?
先來看看程式的部份:
可以與前面的程式做個比較,下面的程式裡,我們取出一個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).
先來看看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
NuGet Gallery : http://nuget.org/List/Packages/Newtonsoft.Json
使用Json.NET就不要使用JsonResult類別傳回,而是需要傳回類別須使用ActionResult,
如果使用JsonResult類別傳回的話,就會有以下的狀況發生:
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,看上面的原始碼)
但是傳回的東西,看起來好像是JSON文件,但是卻無法正確解析…
因為格式不對,雖然傳回的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");}}
Content-Type為指定的「application/json」而且可以正確解析出JSON內容
不過就只有一個Product物件而已,怎麼會從後端傳了一個「1.8MB」的資料呢?
其實答案很簡單,就是因為EF的關係,把有關連的資料也給取了出來,Category', Order_Details, Supplier
在Category這個欄位裡,還把有關聯到的Product資料也帶了出來… 其他像是Order_Details, Supplier也是一樣的情況,
這樣的資料不肥才怪!
簡單的解法就是,當我們要取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而已
但是這樣的作法卻挺麻煩的,物件的欄位少還無所謂,一旦碰上了欄位很多的物件時,這樣手動指定物件欄位的作法就不切實際。
嗯…這篇寫得有點長了,所以下一篇再繼續…
以上
沒有留言:
張貼留言