7 votes

Sérialisation des problèmes d'Entity Framework

Comme plusieurs autres personnes, je rencontre des problèmes pour sérialiser les objets Entity Framework, afin de pouvoir envoyer les données par AJAX dans un format JSON.

J'ai la méthode suivante côté serveur, que j'essaie d'appeler en utilisant AJAX par le biais de 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>();
}

L'appel via AJAX donne lieu à cette erreur :

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

Cela est dû à la manière dont Entity Framework crée des références circulaires pour que tous les objets restent liés et accessibles côté serveur.

Je suis tombé sur le code suivant de ( http://hellowebapps.com/2010-09-26/producing-json-from-entity-framework-4-0-generated-classes/ ) qui prétend contourner ce problème en plafonnant la profondeur maximale des références. J'ai ajouté le code ci-dessous, car j'ai dû le modifier légèrement pour qu'il fonctionne (toutes les parenthèses angulaires sont manquantes dans le code du site web).

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

}

Cependant, même en utilisant ce code, de la manière suivante :

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

Je vois toujours le A circular reference was detected... L'exception est levée !

8voto

Tom Deloford Points 508

J'ai résolu ces problèmes avec les classes suivantes :

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

et

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;

      }
    }
  }

Vous pouvez maintenant faire un appel en toute sécurité comme new EFJavaScriptSerializer().Serialize(obj)

Mise à jour Depuis la version Telerik v1.3+, vous pouvez maintenant surcharger la méthode GridActionAttribute.CreateActionResult et ainsi intégrer facilement ce sérialiseur dans des méthodes de contrôleurs spécifiques en appliquant vos propres règles de sécurité. [GridAction] attribut :

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

et

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

et enfin

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

2voto

Wes Grant Points 979

Vous pouvez également détacher l'objet du contexte et il enlèvera les propriétés de navigation afin qu'il puisse être sérialisé. Pour mes classes de dépôt de données qui sont utilisées avec Json, j'utilise quelque chose comme ceci.

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

Par défaut, l'objet retourné aura toutes les propriétés complexes/navigation, mais en définissant detach = true, il supprimera ces propriétés et retournera uniquement l'objet de base. Pour une liste d'objets, l'implémentation ressemble à ceci

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

1voto

Tom Deloford Points 508

Je viens de tester ce code avec succès.

Il se peut que dans votre cas, votre objet Message se trouve dans une assemblée différente ? La propriété surchargée SupportedTypes renvoie tout UNIQUEMENT dans sa propre assemblée, de sorte que, lorsque l'on appelle serialize, l'option JavaScriptSerializer par défaut, la norme JavaScriptConverter .

Vous devriez être en mesure de vérifier ce débogage.

1voto

Julien REGNARD Points 11

Votre erreur est due à des classes "Reference" générées par EF pour certaines entités avec des relations 1:1 et que le JavaScriptSerializer n'a pas réussi à sérialiser. J'ai utilisé une solution de contournement en ajoutant une nouvelle condition :

    !p.Name.EndsWith("Reference")

Le code pour obtenir les propriétés complexes ressemble à ceci :

    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;

J'espère que cela vous aidera.

1voto

Merlyn Morgan-Graham Points 31815

J'ai eu un problème similaire en poussant ma vue via Ajax vers les composants de l'interface utilisateur.

J'ai également trouvé et essayé d'utiliser l'exemple de code que vous avez fourni. J'ai rencontré quelques problèmes avec ce code :

  • SupportedTypes ne capturait pas les types dont j'avais besoin, donc le convertisseur n'était pas appelé.
  • Si la profondeur maximale est atteinte, la sérialisation sera tronquée.
  • Il a rejeté tous les autres convertisseurs que j'avais sur le sérialiseur existant en créant son propre new JavaScriptSerializer

Voici les corrections que j'ai apportées à ces problèmes :

Réutilisation du même sérialiseur

J'ai simplement réutilisé le sérialiseur existant qui est passé dans le fichier Serialize pour résoudre ce problème. Mais cela a cassé le hack de la profondeur.

Troncature sur les visites déjà effectuées, plutôt que sur la profondeur.

Au lieu de tronquer sur la profondeur, j'ai créé un fichier HashSet<object> d'instances déjà vues (avec une IEqualityComparer qui vérifie l'égalité des références). Je n'effectuais simplement pas de récursion si je trouvais une instance que j'avais déjà vue. Il s'agit du même mécanisme de détection intégré à la méthode JavaScriptSerializer lui-même, donc ça a bien marché.

Le seul problème de cette solution est que le résultat de la sérialisation n'est pas très déterministe. L'ordre de la troncature dépend fortement de l'ordre dans lequel les réflexions trouvent les propriétés. Vous pourriez résoudre ce problème (avec un impact sur les performances) en triant avant de récurrer.

SupportedTypes a besoin des bons types

Mon JavaScriptConverter ne pouvait pas vivre dans la même assemblée que mon modèle. Si vous envisagez de réutiliser ce code de convertisseur, vous rencontrerez probablement le même problème.

Pour résoudre ce problème, j'ai dû pré-traverser l'arbre des objets, en gardant une trace de l'objet. HashSet<Type> de types déjà vus (afin d'éviter ma propre récursion infinie), et je passe cela à la méthode JavaScriptConverter avant de l'enregistrer.

En regardant ma solution, j'utiliserais maintenant des modèles de génération de code pour créer une liste des types d'entités. Cette solution serait beaucoup plus sûre (elle utilise une simple itération) et aurait une bien meilleure performance puisqu'elle produirait une liste au moment de la compilation. Je passerais toujours cette liste au convertisseur pour qu'elle puisse être réutilisée entre les modèles.

Ma solution finale

J'ai jeté ce code et j'ai réessayé :)

J'ai simplement écrit du code pour projeter sur de nouveaux types (types "ViewModel" - dans votre cas, ce serait des types de contrat de service) avant de faire ma sérialisation. L'intention de mon code a été rendue plus explicite, cela m'a permis de sérialiser uniquement les données que je voulais, et il n'y avait pas de risque de glisser des requêtes par accident (par exemple, en sérialisant toute ma base de données).

Mes types étaient assez simples, et je n'avais pas besoin de la plupart d'entre eux pour ma vue. Je pourrais regarder dans AutoMapper pour faire une partie de cette projection dans le futur. .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X