Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializing Entity Framework problems

Like several other people, I'm having problems serializing Entity Framework objects, so that I can send the data over AJAX in a JSON format.

I've got the following server-side method, which I'm attempting to call using AJAX through 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>();
}

Calling this via AJAX results in this error:

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

Which is because of the way the Entity Framework creates circular references to keep all the objects related and accessible server side.

I came across the following code from (http://hellowebapps.com/2010-09-26/producing-json-from-entity-framework-4-0-generated-classes/) which claims to get around this problem by capping the maximum depth for references. I've added the code below, because I had to tweak it slightly to get it work (All angled brackets are missing from the code on the website)

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

}

However even when using that code, in the following way:

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

I'm still seeing the A circular reference was detected... exception being thrown!

like image 877
Peter Bridger Avatar asked Oct 29 '10 15:10

Peter Bridger


3 Answers

I solved these issues with the following classes:

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

and

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;

      }
    }
  }

You can now safely make a call like new EFJavaScriptSerializer().Serialize(obj)

Update : since version Telerik v1.3+ you can now override the GridActionAttribute.CreateActionResult method and hence you can easily integrate this Serializer into specific controller methods by applying your custom [GridAction] attribute:

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

and

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

and finally..

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));
      }
    }
like image 107
Tom Deloford Avatar answered Sep 21 '22 12:09

Tom Deloford


You can also detach the object from the context and it will remove the navigation properties so that it can be serialized. For my data repository classes that are used with Json i use something like this.

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

By default the returned object will have all of the complex/navigation properties, but by setting detach = true it will remove those properties and return the base object only. For a list of objects the implementation looks like this

 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>();
    }
like image 44
Wes Grant Avatar answered Sep 23 '22 12:09

Wes Grant


Your error occured due to some "Reference" classes generated by EF for some entities with 1:1 relations and that the JavaScriptSerializer failed to serialize. I've used a workaround by adding a new condition :

    !p.Name.EndsWith("Reference")

The code to get the complex properties looks like this :

    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;

Hope this help you.

like image 43
Julien REGNARD Avatar answered Sep 22 '22 12:09

Julien REGNARD