Сериализация проблем Entity Framework - PullRequest
7 голосов
/ 29 октября 2010

Как и у некоторых других людей, у меня возникают проблемы с сериализацией объектов Entity Framework, чтобы я мог отправлять данные через AJAX в формате JSON.

У меня есть следующий метод на стороне сервера, который я пытаюсь вызвать, используя AJAX через jQuery

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

Я все еще вижу, как выдается исключение A circular reference was detected...!

Ответы [ 5 ]

8 голосов
/ 03 октября 2011

Я решил эти проблемы с помощью следующих классов:

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 и, следовательно, вы можете легко интегрировать этот Serializer в определенные методы контроллера, применяя свой собственный атрибут [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));
      }
    }
2 голосов
/ 10 июня 2011

Вы также можете отсоединить объект от контекста, и он удалит свойства навигации, чтобы его можно было сериализовать.Для моих классов хранилища данных, которые используются с 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;
        }
    }

По умолчанию возвращаемый объект будет иметь все свойства complex / navigation, но установив 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 голос
/ 27 сентября 2011

У меня была похожая проблема с передачей моего представления через Ajax к компонентам пользовательского интерфейса.

Я также нашел и попытался использовать предоставленный вами пример кода.У меня были некоторые проблемы с этим кодом:

  • SupportedTypes не захватывал нужные мне типы, поэтому конвертер не вызывался
  • Если достигнута максимальная глубина,сериализация была бы усечена
  • Он выбросил любые другие конвертеры, которые у меня были на существующем сериализаторе, создав свой собственный new JavaScriptSerializer

Вот исправления, которые я реализовал для этих проблем:

Повторное использование того же сериализатора

Я просто повторно использовал существующий сериализатор, который передается в Serialize, чтобы решить эту проблему.Это сломало хак глубины.

Усечение по уже посещенному, а не по глубине

Вместо усечения по глубине я создал HashSet<object> из уже увиденныхэкземпляры (с пользовательским IEqualityComparer, который проверял равенство ссылок).Я просто не повторял, нашел ли я экземпляр, который уже видел.Это тот же механизм обнаружения, который встроен в сам JavaScriptSerializer, поэтому он работал довольно хорошо.

Единственная проблема с этим решением состоит в том, что выход сериализации не очень детерминирован.Порядок усечения сильно зависит от порядка, в котором отражения находят свойства.Вы можете решить эту проблему (с помощью перфоманса), отсортировав перед повторением.

SupportedTypes нужны правильные типы

Мой JavaScriptConverter не может находиться в одной сборкекак моя модель.Если вы планируете использовать этот код преобразователя повторно, вы, вероятно, столкнетесь с той же проблемой.

Чтобы решить эту проблему, мне пришлось предварительно пройтись по дереву объектов, сохранив HashSet<Type> уже просмотренных типов (чтобы избежатьмоя собственная бесконечная рекурсия), и перед тем, как зарегистрировать ее, передайте ее JavaScriptConverter.

Оглядываясь назад на свое решение, я бы теперь использовал шаблоны генерации кода для создания списка типов сущностей.Это было бы намного более надежно (это использует простую итерацию) и имело бы намного лучшую производительность, так как это произвело бы список во время компиляции.Я бы все же передал это конвертеру, чтобы его можно было повторно использовать между моделями.

Мое окончательное решение

Я выбросил этот код и попытался снова:)

Я просто написал код для проецирования на новые типы (типы "ViewModel" - в вашем случае это были бы типы контрактов на обслуживание) перед выполнением моей сериализации.Намерение моего кода было сделано более явным, оно позволило мне сериализовать только те данные, которые я хотел, и у него не было возможности проскальзывать в запросах при аварии (например, при сериализации всей моей БД).типы были довольно просты, и мне не нужно было большинство из них на мой взгляд.Я мог бы заглянуть в AutoMapper, чтобы сделать часть этой проекции в будущем .

1 голос
/ 07 мая 2011

Ваша ошибка произошла из-за некоторых «Справочных» классов, сгенерированных 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 голос
/ 02 апреля 2011

Я только что успешно протестировал этот код.

Может быть, в вашем случае ваш объект сообщения находится в другой сборке? Переопределенное свойство SupportedTypes возвращает все ONLY в своей сборке, поэтому при вызове сериализации JavaScriptSerializer по умолчанию соответствует стандарту JavaScriptConverter.

Вы должны быть в состоянии проверить эту отладку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...