Search code examples
c#entity-frameworkjavascriptserializer

ScriptIgnore tag being ignored even with ApplyToOverrides = true... and it works in LinqPad


Basically, I'm trying to send a simple entity into json using JavaScriptSerializer. Yes, I know you want me to make a redundant class for that and shove it through AutoMapper and I'm asking for trouble. Humour me.

I'm using Entity Framework 6 to fetch a simple object to fetch a simple object.

Here's my test code:

    [TestMethod]
    public void TestEntityTest()
    {
        var db = new TestDbContext();
        var ent = db.ResupplyForms.SingleOrDefault(e => e.Guid == new Guid("55117161-F3FA-4291-8E9B-A67F3B416097"));
        var json = new JavaScriptSerializer().Serialize(ent);
    }

Pretty straight forward. Fetch the thing and serialize it.

It errors out with the following:

An exception of type 'System.InvalidOperationException' occurred in System.Web.Extensions.dll but was not handled in user code

Additional information: A circular reference was detected while serializing an object of type 'System.Data.Entity.DynamicProxies.ResupplyForm_13763C1B587B4145B35C75CE2D5394EBED19F93943F42503204F91E0B9B4294D'.

Here's the entity:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using System.Data.Entity.Spatial;
using Rome.Model.DataDictionary;
using System.Web.Script.Serialization;

namespace Rome.Model.Form
{
    [Table(nameof(ResupplyForm))]
    public partial class ResupplyForm
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int ResupplyFormID {get;set;}

        public Guid Guid {get;set;}

        public int? RecordStatus {get;set;}

        [ForeignKey(nameof(RecordStatus))]
        [ScriptIgnore(ApplyToOverrides = true)]
        public virtual LookupItem RecordStatusLookupItem {get;set;} 
    }
}

I'll leave out the def for LookupItem because that gets into the schema of the whole rest of my project and there's no sane world in which that should matter, since I already flagged it as "ignored".

And here's a super-simple context:

public class TestDbContext : DbContext
{
    public TestDbContext()
        : base("data source=.;initial catalog=studybaseline;integrated security=True;pooling=False;multipleactiveresultsets=False")
    {

    }

    public virtual DbSet<ResupplyForm> ResupplyForms { get; set; }
}

And now, the coup de gras: A LinqPad query that runs perfectly, using the exact same code as my snippet:

var db = new Rome.Model.Form.TestDbContext();
var ent = db.ResupplyForms.SingleOrDefault(e => e.Guid == new Guid("55117161-F3FA-4291-8E9B-A67F3B416097"));
var json = new JavaScriptSerializer().Serialize(ent).Dump();

Which happily returns

{"ResupplyFormID":1,"Guid":"55117161-f3fa-4291-8e9b-a67f3b416097","RecordStatus":null}

I have been pulling my hair out all day on this one, so any help is appreciated.


Solution

  • Okay, I've dug further into this a day later and found the cause: it has nothing to do with the [ScriptIgnore(ApplyToOverrides = true)] things. It has to do with the EntityProxy subclass that Entity Framework creates for every entity. My ResupplyForm isn't actually used as a ResupplyForm in my tests... instead it's an EntityProxy subclass.

    This EntityProxy subclass adds a new member, _entityWrapper. If the EntityProxy is wrapping a class with no navigational properties, _entityWrapper doesn't contain any cycles... but as soon as you add a navigational property, the _entityWrapper contains cycles, which breaks serialization.

    Vague error messages ruin everything. If the JavaScriptSerializer told me which field was bad, I could've saved a lot of time.

    Anyhow, I should look at switching to NewtonSoft, but that's created its own problems (for another post) but instead I've created a very crude workaround:

    public static class JsonSerializerExtensions
        {
            /// <summary>
            /// Convert entity to JSON without blowing up on cyclic reference.
            /// </summary>
            /// <param name="target">The object to serialize</param>
            /// <param name="entityTypes">Any entity-framework-related types that might be involved in this serialization.  If null, it will only use the type of "target".</param>
            /// <param name="ignoreNulls">Whether nulls should be serialized or not.</param>
            /// <returns>Json</returns>
            /// <remarks>This requires some explanation: all POCOs used by entites aren't their true form.  
            /// They're subclassed proxies of the object you *think* you're defining.  These add a new member
            /// _EntityWrapper, which contains cyclic references that break the javascript serializer.
            /// This is Json Serializer function that skips _EntityWrapper for any type in the entityTypes list.
            /// If you've a complicated result object that *contains* entities, forward-declare them with entityTypes.
            /// If you're just serializing one object, you can omit entityTypes.
            ///</remarks>
            public static string ToJsonString(this object target, IEnumerable<Type> entityTypes = null, bool ignoreNulls = true)
            {
                var javaScriptSerializer = new JavaScriptSerializer();
                if(entityTypes == null)
                {
                    entityTypes = new[] { target.GetType() };
                }
                javaScriptSerializer.RegisterConverters(new[] { new EntityProxyConverter(entityTypes, ignoreNulls) });
                return javaScriptSerializer.Serialize(target);
            }
        }
    
        public class EntityProxyConverter : JavaScriptConverter
        {
            IEnumerable<Type> _EntityTypes = null;
            bool _IgnoreNulls;
            public EntityProxyConverter(IEnumerable<Type> entityTypes, bool ignoreNulls = true) : base()
            {
                _EntityTypes = entityTypes;
                _IgnoreNulls = ignoreNulls;
            }
    
            public override IEnumerable<Type> SupportedTypes
            {
                get
                {
                    return _EntityTypes;
                }
            }
    
            public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
            {
                throw new NotImplementedException();
            }
    
            public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
            {
                var result = new Dictionary<string, object>();
                if (obj == null)
                {
                    return result;
                }
                var properties = obj.GetType().GetProperties(
                    System.Reflection.BindingFlags.Public 
                    | System.Reflection.BindingFlags.Instance 
                    | System.Reflection.BindingFlags.GetProperty
                );
                foreach (var propertyInfo in properties.Where(p => Attribute.GetCustomAttributes(p, typeof(ScriptIgnoreAttribute), true).Length == 0))
                {
                    if (!propertyInfo.Name.StartsWith("_"))
                    {
                        var value = propertyInfo.GetValue(obj, null);
                        if (value != null || !_IgnoreNulls)
                        {
                            result.Add(propertyInfo.Name, propertyInfo.GetValue(obj, null));
                        }
                    }
                }
                return result;
            }
        }
    

    You have to pass in the classes (which, let's remember, are on-the-fly generated proxy classes) to use it, sadly, so it will probably fail miserably for any reasonable object-graph, but it will work for simple single objects and arrays and the like. It also fails for use with JsonResult because these overrides can't be used there.