2011年10月26日 星期三

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


接續「使用Entity Framework 將物件轉為JSON時遇到循環參考錯誤 2

上一篇是說明如何去修改Edmx的屬性「消極式載入已啟用」設定

以及在不修改Edmx的屬性「消極式載入已啟用」的情況下於程式中將LazyLoadingEnable設定為false,

透過這些的修改方式讓物件於序列化為JSON時不去包含或是不載入關連的物件資料。

 

而這一篇就來看看使用自定義的JavaScriptConverter類別,在不修改上述兩項設定的情形下,

如何做到只序列化物件本身的資料以及序列化物件本身以及其關連的物件資料。


自定義新的JavaScriptConvert類別

那如果我們什麼都不想修改的話呢?還是有辦法的,因為問題是發生在JavaScriptConveret類別上,

所以找了一下在網路上的資料,發現到了一篇文章裡有提供一個方法,去自定義一個類別並繼承JavaScriptConvert類別,

然後去覆寫序列化的方法,

參考連結:

LUKIYA'S NEVERLAND - 使用 Entity Framework 返回 JsonResult 时循环引用的避免。

EFSimpleJavaScriptConverter」類別的程式內容:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Script.Serialization;
namespace Test.Helper
{
  public class EFSimpleJavaScriptConverter : JavaScriptConverter
  {
    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
      throw new NotImplementedException();
    }
    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
      IDictionary<string, object> result = new Dictionary<string, object>();
      Type type = obj.GetType();
      PropertyInfo[] properties = type.GetProperties();
      foreach (PropertyInfo property in properties)
      {
        bool allowSerialize = IsAllowSerialize(property);
        if (allowSerialize)
        {
          result[property.Name] = property.GetValue(obj, null);
        }
      }
      return result;
    }
    private bool IsAllowSerialize(PropertyInfo property)
    {
      object[] attrs = property.GetCustomAttributes(true);
      foreach (object attr in attrs)
      {
        if (attr is System.Data.Objects.DataClasses.EdmScalarPropertyAttribute)
        {
          return true;
        }
      }
      return false;
    }
    public override IEnumerable<Type> SupportedTypes
    {
      get
      {
        yield return typeof(System.Data.Objects.DataClasses.EntityObject);
      }
    }
  }
}

因為這是個產出比較精簡的JSON資料,所以我就改了類別名稱為「EFSimpleJavaScriptConvert

而這個類別在Controller – Action的使用方法如下:

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));
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    serializer.RegisterConverters(new List<EFSimpleJavaScriptConverter>() { new EFSimpleJavaScriptConverter() });
    string jsonContent = serializer.Serialize(product);
    return new ContentResult { Content = jsonContent, ContentType = "application/json" };
  }
}

執行的結果:

image_thumb[15]

上面所產生的結果就真的很簡單,就是只有將物件本身的欄位給序列化出來,而其餘關連資料或是EF所自動產出的資料都不會出現。

在上面的「EFSimpleJavaScriptConverter」類別中會只輸出物件本身欄位的資料,

是去判斷物件每個Propertry的「EdmScalarPropertyAttribute」,所以就不會把關連的物件給抓出來做序列化。

MSDN - EdmScalarPropertyAttribute 類別

表示此屬性 (Property) 代表純量屬性 (Property)。
EdmScalarPropertyAttribute 會將資料類別的純量屬性連結到概念模型中所定義之實體類型或複雜類型的屬性

序列化時也加入關連的資料

如果說上面只序列化物件本身的欄位資料就太過於精簡,像Product物件也想要知道關連的Category物件的CategoryName等,

如此就可以不用另外再去取一次Category物件資料的動作,

所以就根據網路上的兩篇文件的內容另外建立了一個「EFJavaScriptConvertor」類別,

再這個類別中可以指定要取得深度多少的資料,資料深度表示關連物件的深度,

像Product的物件本身以及包含的關聯物件的深度就是1,而關連物件的Category物件的關聯資料深度就是2

參考連結:

Stackoverflow – Serializing Entity Framework problems

HelloWebApps - Producing JSON from Entity Framework 4.0 generated classes

EFJavaScriptConverter」類別

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Script.Serialization;
namespace Test.Helper
{
  public class EFJavaScriptConverter : JavaScriptConverter
  {
    private int _currentDepth = 1;
    private readonly int _maxDepth = 1;
    private readonly List<object> _processedObjects = new List<object>();
    private readonly Type[] _builtInTypes = new[]
    {
      typeof(bool),
      typeof(byte),
      typeof(sbyte),
      typeof(char),
      typeof(decimal),
      typeof(double),
      typeof(float),
      typeof(int),
      typeof(uint),
      typeof(long),
      typeof(ulong),
      typeof(short),
      typeof(ushort),
      typeof(string),
      typeof(DateTime),
      typeof(Guid),
      typeof(bool?),
      typeof(byte?),
      typeof(sbyte?),
      typeof(char?),
      typeof(decimal?),
      typeof(double?),
      typeof(float?),
      typeof(int?),
      typeof(uint?),
      typeof(long?),
      typeof(ulong?),
      typeof(short?),
      typeof(ushort?),
      typeof(DateTime?),
      typeof(Guid?)
    };
    public EFJavaScriptConverter() : this(1, null) 
    { 
    
    }
    public EFJavaScriptConverter(int maxDepth = 1, EFJavaScriptConverter parent = null)
    {
      _maxDepth = maxDepth;
      if (parent != null)
      {
        _currentDepth += parent._currentDepth;
      }
    }
    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
      return null;
    }
    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
      _processedObjects.Add(obj.GetHashCode());
      var type = obj.GetType();
      var properties = from p in type.GetProperties()
               where
                p.CanRead && 
                p.GetIndexParameters().Count() == 0 &&
                _builtInTypes.Contains(p.PropertyType)
               select p;
      var result = properties.ToDictionary(
              p => p.Name,
              p => (Object)TryGetStringValue(p, obj));
      if (_maxDepth >= _currentDepth)
      {
        var complexProperties = from p in type.GetProperties()
                    where 
                      p.CanRead &&
                      p.CanWrite &&
                      !p.Name.EndsWith("Reference") &&
                      !_builtInTypes.Contains(p.PropertyType) &&
                      !AllreadyAdded(p, obj) &&
                      !_processedObjects.Contains(p.GetValue(obj, null) == null
                        ? 0
                        : p.GetValue(obj, null).GetHashCode())
                    select p;
        foreach (var property in complexProperties)
        {
          var complexValue = TryGetValue(property, obj);
          if (complexValue != null)
          {
            var js = new EFJavaScriptConverter(_maxDepth - _currentDepth, this);
            result.Add(property.Name, js.Serialize(complexValue, new EFJavaScriptSerializer()));
          }
        }
      }
      return result;
    }
    private bool AllreadyAdded(PropertyInfo p, object obj)
    {
      var val = TryGetValue(p, obj);
      return _processedObjects.Contains(val == null ? 0 : val.GetHashCode());
    }
    private static object TryGetValue(PropertyInfo p, object obj)
    {
      var parameters = p.GetIndexParameters();
      if (parameters.Length == 0)
      {
        return p.GetValue(obj, null);
      }
      else
      {
        //can't serialize these
        return null;
      }
    }
    private static object TryGetStringValue(PropertyInfo p, object obj)
    {
      if (p.GetIndexParameters().Length == 0)
      {
        var val = p.GetValue(obj, null);
        return val;
      }
      else
      {
        return string.Empty;
      }
    }
    public override IEnumerable<Type> SupportedTypes
    {
      get
      {
        var types = new List<Type>();
        //ef types
        types.AddRange(Assembly.GetAssembly(typeof(System.Data.Objects.DataClasses.EntityObject)).GetTypes());
        return types;
      }
    }
  }
}

在Controller/Action中的使用:

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));
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    serializer.RegisterConverters(new List<EFJavaScriptConverter>() { new EFJavaScriptConverter(1) });
    string jsonContent = serializer.Serialize(product);
    return new ContentResult { Content = jsonContent, ContentType = "application/json" };
  }
}

因為EFJavaScriptConvert的預設建構式就已經指定深度為2,所以就可以取得關連物件的資料,

執行結果:

可以看到有把關連資料給取出來,但是關連物件的關聯就沒有取出來

image_thumb[24]

使用EFSimpleJavaScriptConvert與EFJavaScriptConverter

我們想要把這兩個類別給放到專案中去使用,因為有時候是指需要序列化物件本身不包含關連物件,

而有時候是需要序列化物件本身與其關連的物件,然後再去簡化建立JavaScriptSerializer以及RegisterConverters的操作,

所以就建立一個「EFJavaScriptSerialiser」類別,在Controller/Action裡去使用時就會簡潔許多。

EFJavaScriptSerialiser」類別

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Script.Serialization;
namespace Test.Helper
{
  public class EFJavaScriptSerializer : JavaScriptSerializer
  {
    public EFJavaScriptSerializer()
    {
      RegisterConverters(new List<JavaScriptConverter> { new EFSimpleJavaScriptConverter() });
    }
    public EFJavaScriptSerializer(int maxDepth = 1, EFJavaScriptConverter parent = null)
    {
      RegisterConverters(new List<JavaScriptConverter> { new EFJavaScriptConverter(maxDepth, parent) });
    }
  }
}

使用方式一:

使用EFSimpleJavaScriptConverter類別

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));
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    string jsonContent = new EFJavaScriptSerializer().Serialize(product);
    return new ContentResult { Content = jsonContent, ContentType = "application/json" };
  }
}

執行結果:

image_thumb[29]

使用方法二:

使用EFJavaScriptConverter,指定深度為1

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));
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    string jsonContent = new EFJavaScriptSerializer(1).Serialize(product);
    return new ContentResult { Content = jsonContent, ContentType = "application/json" };
  }
}

執行結果:

image_thumb[32]

使用方法三:

使用EFJavaScriptConverter,指定深度為3

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));
  }
  else
  {
    ProductService service = new ProductService();
    var product = service.Single(id.Value);
    string jsonContent = new EFJavaScriptSerializer(3).Serialize(product);
    return new ContentResult { Content = jsonContent, ContentType = "application/json" };
  }
}

執行結果:

image_thumb[37]

 

以上

沒有留言:

張貼留言

提醒

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