序列化Entity Framework问题

7

像其他一些人一样,我在序列化实体框架对象方面遇到了问题,以便我可以将数据以JSON格式通过AJAX发送。

我有以下服务器端方法,我正在尝试使用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

这是因为Entity Framework创建循环引用以保持所有对象在服务器端相关和可访问。
我发现了来自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);

我仍然看到抛出了检测到循环引用...异常!


可能是将Entity Framework对象序列化为JSON的重复问题。 - Craig Stuntz
这个问题涉及匿名类型作为解决方案,而我正在尝试不同的方法来解决它。 - Peter Bridger
你不想将投影到匿名类型(或静态定义类型)吗?你的复杂类型肯定需要以某种方式展开。我不确定为什么上面的转换器不起作用 - 你能否发布你尝试序列化的“Message”类的代码?另外 - 你仍然在LINQ查询中推迟查询的执行,请在“AsEnumerable()”调用后调用“ToList()”。 - David Neale
1
David - 有很多有效的理由不想在每个实体访问上进行投影。DRY首先要考虑..考虑你有一个包含40个简单属性和仅产生循环引用的1个复杂类型属性的实体! - Tom Deloford
5个回答

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

看起来不错 :) 我很快会仔细查看代码。我认为可以进行一些性能/逻辑上的改进(因为我之前写过类似的代码,我采用了另一种方法)。如果你愿意,我可以稍后给你一些反馈,或者编辑代码,让你决定是否要回滚。另外,RelationshipManager是什么?应该删除吗? - Merlyn Morgan-Graham
是的,请提供性能反馈/建议。在我的情况下,它是一次性/小规模对象序列化的一部分,因此性能并不关键。我相信'RelationshipManager'是添加到EF4 DynamicProxies中的属性(可能仅限于POCO模式),如果您删除该行,则会将该属性序列化为您可能不想要的JSON。 - Tom Deloford
我认为你应该能够传入支持的类型,还可以传入一个故意忽略的类型列表(它“处理”但是被忽略的类型)。如果您正在根据模型生成类型,则支持的类型列表可以由T4模板生成,或者手动创建相对容易。此外,仅获取哈希码不足以建立唯一性。您还必须检查Equals。我建议使用HashSet<object>和一个返回正常哈希码并检查引用相等性而不是调用EqualsIEqualityComparer<object> - Merlyn Morgan-Graham
此外,我认为在支持的类型上忽略了 maxDepth,因为当您进行递归时会创建一个新的序列化程序。不确定在这一点上限制深度的代码有多少用处。也许您应该删除它,而不是创建一个新实例,只需传递您在 Serialize 中获得的实例即可。 - Merlyn Morgan-Graham
实际上没有Merlyn。创建的新序列化程序纯粹是为了满足接口,如果您仔细查看,它在任何时候都不使用传递的序列化程序参数,它只使用EFJavaScriptConverter js中的maxDepth。 - Tom Deloford
如果是这种情况,那么你可以安全地传递现有的参数,尽管在那时你是正确的,这只是一个小问题。我将对代码进行一些更改,并稍后将其放在粘贴板上供您查看,让您决定是否喜欢这些更改。 - Merlyn Morgan-Graham

2
你也可以将对象从上下文中分离,这样它就会删除导航属性,以便可以进行序列化。对于我用于Json的数据存储库类,我使用以下代码:

你也可以将对象从上下文中分离,这样它就会删除导航属性,以便可以进行序列化。对于我用于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>();
    }

1
我其实喜欢这种方法,但有时我需要能够序列化超过1个层级。 - Tom Deloford
这正是我正在寻找的 - 至少是 DataContext.Detach() 方法。这消除了“对象已释放”异常... - Cody
在EF6中,通过设置“EntityState”进行分离后,它将具有2级深度。 - Martin Braun

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;

希望这能帮到你。


1

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

也许在你的情况下,你的消息对象位于不同的程序集中?重写的属性SupportedTypes仅在自己的程序集中返回所有内容,因此当调用序列化时,JavaScriptSerializer默认使用标准的JavaScriptConverter

你应该能够通过调试来验证这一点。


+1;我曾经遇到过这个问题......我在我的回答中提供了一个解决方案。虽然没有代码。但我抛弃了它,因为我采用了涉及新类型投射的解决方案。 - Merlyn Morgan-Graham

1

我曾经遇到过一个类似的问题,就是通过Ajax将我的视图推送到UI组件。

我也尝试使用你提供的代码示例。但是我遇到了一些问题:

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

以下是我为这些问题实施的修复措施:

重用同一序列化程序

我只需重用传递给 Serialize 的现有序列化程序即可解决此问题。但是这会破坏深度hack。

在已访问的对象上截断,而不是在深度上截断

我创建了一个 HashSet<object> 来存储已经看到的实例(使用自定义的 IEqualityComparer 检查引用相等性)。如果我发现已经看到了一个实例,我就不会进行递归。这与 JavaScriptSerializer 本身内置的检测机制相同,因此效果非常好。

这个解决方案唯一的问题是序列化输出不太确定。截断的顺序强烈依赖于反射找到属性的顺序。你可以通过在递归之前排序来解决这个问题(会影响性能)。
SupportedTypes需要正确的类型
我的JavaScriptConverter不能与我的模型在同一个程序集中。如果你计划重用这个转换器代码,你可能会遇到同样的问题。
为了解决这个问题,我必须预先遍历对象树,保持已经看到的类型的HashSet(避免自己的无限递归),并将其传递给JavaScriptConverter在注册之前。
回顾我的解决方案,现在我会使用代码生成模板来创建实体类型列表。这将更加可靠(它使用简单的迭代),并且由于它会在编译时生成列表,因此性能会更好。我仍然会将其传递给转换器,以便在模型之间重复使用。
我的最终解决方案
我抛弃了那段代码,重新开始尝试 :)

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

我的类型相当简单,我不需要大部分用于视图。我可能会研究AutoMapper以便在未来进行一些投影


我同意投影是一种更安全的方法,即使它可能需要更多的代码,但不必担心序列化问题真的很好。您可以尝试尝试我发布的代码,因为我和您一样遇到了完全相同的问题,我相信我的解决方案可以避免不必要的投影。 - Tom Deloford
@Tom:是的,我认为提供这两个选项是一件好事。通常我的视图与我的视图模型不匹配,我想从客户端中删除不重要的数据,因此投影是有意义的。其他时候,我没有这些要求,我的模型很小,大部分数据都是相关的,因此序列化是有意义的。 - Merlyn Morgan-Graham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接