序列化实体框架问题

时间:2010-10-29 15:11:27

标签: entity-framework serialization json

和其他几个人一样,我在序列化Entity Framework对象时遇到问题,因此我可以通过AJAX以JSON格式发送数据。

我有以下服务器端方法,我试图通过jQuery使用AJAX调用

[WebMethod]
public static IEnumerable<Message> GetAllMessages(int officerId)
{

        SIBSv2Entities db = new SIBSv2Entities();

        return  (from m in db.MessageRecipients
                        where m.OfficerId == officerId
                        select m.Message).AsEnumerable<Message>();
}

通过AJAX调用此结果会导致此错误:

A circular reference was detected while serializing an object of type \u0027System.Data.Metadata.Edm.AssociationType

这是因为实体框架创建循环引用的方式使所有对象保持相关且可访问服务器端。

我遇到了来自(http://hellowebapps.com/2010-09-26/producing-json-from-entity-framework-4-0-generated-classes/)的以下代码,声称通过限制引用的最大深度来解决此问题。 我已经添加了下面的代码,因为我必须稍微调整才能使其正常工作(网站上的代码中缺少所有有角度的括号)

using System.Web.Script.Serialization;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System;


public class EFObjectConverter : JavaScriptConverter
{
  private int _currentDepth = 1;
  private readonly int _maxDepth = 2;

  private readonly List<int> _processedObjects = new List<int>();

  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)
  };

  public EFObjectConverter( int maxDepth = 2,
                            EFObjectConverter 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());
    Type type = obj.GetType();
    var properties = from p in type.GetProperties()
                      where p.CanWrite &&
                            p.CanWrite &&
                            _builtInTypes.Contains(p.PropertyType)
                      select p;
    var result = properties.ToDictionary(
                  property => property.Name,
                  property => (Object)(property.GetValue(obj, null)
                              == null
                              ? ""
                              :  property.GetValue(obj, null).ToString().Trim())
                  );
    if (_maxDepth >= _currentDepth)
    {
      var complexProperties = from p in type.GetProperties()
                                where p.CanWrite &&
                                      p.CanRead &&
                                      !_builtInTypes.Contains(p.PropertyType) &&
                                      !_processedObjects.Contains(p.GetValue(obj, null)
                                        == null
                                        ? 0
                                        : p.GetValue(obj, null).GetHashCode())
                              select p;

      foreach (var property in complexProperties)
      {
        var js = new JavaScriptSerializer();

          js.RegisterConverters(new List<JavaScriptConverter> { new EFObjectConverter(_maxDepth - _currentDepth, this) });

        result.Add(property.Name, js.Serialize(property.GetValue(obj, null)));
      }
    }

    return result;
  }

  public override IEnumerable<System.Type> SupportedTypes
  {
    get
    {
      return GetType().Assembly.GetTypes();
    }
  }

}

但即使使用该代码,也是如下:

    var js = new System.Web.Script.Serialization.JavaScriptSerializer();
    js.RegisterConverters(new List<System.Web.Script.Serialization.JavaScriptConverter> { new EFObjectConverter(2) });
    return js.Serialize(messages);

我仍然看到引发A circular reference was detected...异常!

5 个答案:

答案 0 :(得分:8)

我用以下类解决了这些问题:

public class EFJavaScriptSerializer : JavaScriptSerializer
  {
    public EFJavaScriptSerializer()
    {
      RegisterConverters(new List<JavaScriptConverter>{new EFJavaScriptConverter()});
    }
  }

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(int?),
      typeof(double?),
      typeof(bool?),
      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(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.GetIndexParameters().Count() == 0 &&
                                      !_builtInTypes.Contains(p.PropertyType) &&
                                      p.Name != "RelationshipManager" &&
                                      !AllreadyAdded(p, obj)
                                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
      {
        //cant 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(DbContext)).GetTypes());
        //model types
        types.AddRange(Assembly.GetAssembly(typeof(BaseViewModel)).GetTypes());


        return types;

      }
    }
  }

您现在可以安全地拨打new EFJavaScriptSerializer().Serialize(obj)

等电话

更新:自Telerik v1.3 +版本起,您现在可以覆盖GridActionAttribute.CreateActionResult方法,因此您可以通过应用自定义[GridAction]属性轻松地将此序列化程序集成到特定的控制器方法中:

[Grid]
public ActionResult _GetOrders(int id)
{ 
   return new GridModel(Service.GetOrders(id));
}

public class GridAttribute : GridActionAttribute, IActionFilter
  {    
    /// <summary>
    /// Determines the depth that the serializer will traverse
    /// </summary>
    public int SerializationDepth { get; set; } 

    /// <summary>
    /// Initializes a new instance of the <see cref="GridActionAttribute"/> class.
    /// </summary>
    public GridAttribute()
      : base()
    {
      ActionParameterName = "command";
      SerializationDepth = 1;
    }

    protected override ActionResult CreateActionResult(object model)
    {    
      return new EFJsonResult
      {
       Data = model,
       JsonRequestBehavior = JsonRequestBehavior.AllowGet,
       MaxSerializationDepth = SerializationDepth
      };
    }
}

最后..

public class EFJsonResult : JsonResult
  {
    const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";

    public EFJsonResult()
    {
      MaxJsonLength = 1024000000;
      RecursionLimit = 10;
      MaxSerializationDepth = 1;
    }

    public int MaxJsonLength { get; set; }
    public int RecursionLimit { get; set; }
    public int MaxSerializationDepth { 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(JsonRequest_GetNotAllowed);
      }

      var 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)
      {
        var serializer = new JavaScriptSerializer
        {
          MaxJsonLength = MaxJsonLength,
          RecursionLimit = RecursionLimit
        };

        serializer.RegisterConverters(new List<JavaScriptConverter> { new EFJsonConverter(MaxSerializationDepth) });

        response.Write(serializer.Serialize(Data));
      }
    }

答案 1 :(得分:2)

您还可以从上下文中分离对象,它将删除导航属性,以便可以序列化它。对于与Json一起使用的数据存储库类,我使用类似的东西。

 public DataModel.Page GetPage(Guid idPage, bool detach = false)
    {
        var results = from p in DataContext.Pages
                      where p.idPage == idPage
                      select p;

        if (results.Count() == 0)
            return null;
        else
        {
            var result = results.First();
            if (detach)
                DataContext.Detach(result);
            return result;
        }
    }

默认情况下,返回的对象将具有所有复杂/导航属性,但通过设置detach = true,它将删除这些属性并仅返回基础对象。对于对象列表,实现看起来像这样

 public List<DataModel.Page> GetPageList(Guid idSite, bool detach = false)
    {
        var results = from p in DataContext.Pages
                      where p.idSite == idSite
                      select p;

        if (results.Count() > 0)
        {
            if (detach)
            {
                List<DataModel.Page> retValue = new List<DataModel.Page>();
                foreach (var result in results)
                {
                    DataContext.Detach(result);
                    retValue.Add(result);
                }
                return retValue;
            }
            else
                return results.ToList();

        }
        else
            return new List<DataModel.Page>();
    }

答案 2 :(得分:1)

我刚刚成功测试了这段代码。

在您的情况下,您的Message对象可能位于不同的程序集中?覆盖属性SupportedTypes将返回其自己的程序集中的所有 ONLY ,因此在调用serialize时,JavaScriptSerializer默认为标准JavaScriptConverter

您应该能够验证此调试。

答案 3 :(得分:1)

由于EF为一些具有1:1关系的实体生成的“引用”类而且JavaScriptSerializer无法序列化,因此发生了错误。 我通过添加新条件使用了一种解决方法:

    !p.Name.EndsWith("Reference")

获取复杂属性的代码如下所示:

    var complexProperties = from p in type.GetProperties()
                                    where p.CanWrite &&
                                          p.CanRead &&
                                          !p.Name.EndsWith("Reference") &&
                                          !_builtInTypes.Contains(p.PropertyType) &&
                                          !_processedObjects.Contains(p.GetValue(obj, null)
                                            == null
                                            ? 0
                                            : p.GetValue(obj, null).GetHashCode())
                                    select p;

希望这对你有所帮助。

答案 4 :(得分:1)

我有一个类似的问题,将我的视图通过Ajax推送到UI组件。

我还发现并尝试使用您提供的代码示例。我对该代码遇到的一些问题:

  • SupportedTypes没有抓住我需要的类型,因此没有调用转换器
  • 如果达到最大深度,序列化将被截断
  • 通过创建自己的new JavaScriptSerializer
  • ,它抛弃了我在现有序列化程序上的任何其他转换器

以下是我针对这些问题实施的修补程序:

重复使用相同的序列化程序

我只是重用传递给Serialize的现有序列化程序来解决这个问题。这打破了深度黑客。

截断已访问过的内容,而不是深度

我没有截断深度,而是创建了HashSet<object>已见过的实例(使用自定义IEqualityComparer检查引用相等性)。如果我找到一个我已经看过的实例,我根本就没有递归。这与JavaScriptSerializer本身内置的检测机制相同,因此效果很好。

此解决方案的唯一问题是序列化输出不是非常确定。截断的顺序很大程度上取决于反射查找属性的顺序。您可以通过在递归之前进行排序来解决此问题(使用perf命中)。

SupportedTypes需要正确的类型

我的JavaScriptConverter无法与我的模型生活在同一个程序集中。如果您打算重用此转换器代码,则可能会遇到同样的问题。

要解决这个问题,我必须预先遍历对象树,保留已经看到的HashSet<Type>类型(以避免我自己的无限递归),并在注册之前将其传递给JavaScriptConverter

回顾我的解决方案,我现在将使用代码生成模板来创建实体类型列表。这将更加万无一失(它使用简单的迭代),并且具有更好的性能,因为它会在编译时生成列表。我仍然将它传递给转换器,因此可以在模型之间重复使用。

我的最终解决方案

我抛弃了那段代码并再次尝试:)

在编写序列化之前,我只是编写代码来投射到新类型(“ViewModel”类型 - 在您的情况下,它将是服务契约类型)。我的代码的意图更明确,它允许我序列化我想要的数据,并且它没有在事故查询中滑倒的可能性(例如序列化我的整个数据库)。

我的类型相当简单,我不需要大部分内容供我查看。我可以look into AutoMapper to do some of this projection in the future