JSON.NET Error Self referencing loop detected for type

后端 未结 25 2823
我在风中等你
我在风中等你 2020-11-22 02:16

I tried to serialize POCO class that was automatically generated from Entity Data Model .edmx and when I used

JsonConvert.SerializeObject 

25条回答
  •  隐瞒了意图╮
    2020-11-22 02:35

    I was facing the same problem and I tried using JsonSetting to ignore the self-referencing error its kinda work until I got a class which self-referencing very deeply and my dot-net process hangs on Json writing value.

    My Problem

        public partial class Company : BaseModel
    {
        public Company()
        {
            CompanyUsers = new HashSet();
        }
    
        public string Name { get; set; }
    
        public virtual ICollection CompanyUsers { get; set; }
    }
    
    public partial class CompanyUser
    {
        public int Id { get; set; }
        public int CompanyId { get; set; }
        public int UserId { get; set; }
    
        public virtual Company Company { get; set; }
    
        public virtual User User { get; set; }
    }
    
    public partial class User : BaseModel
    {
        public User()
        {
            CompanyUsers = new HashSet();
        }
    
        public string DisplayName { get; set; }
        public virtual ICollection CompanyUsers { get; set; }
    
    }
    

    You can see the problem in User class it's referencing to CompanyUser class which is a self-referencing.

    Now, I'm calling the GetAll Method which includes all the relational properties.

    cs.GetAll("CompanyUsers", "CompanyUsers.User");
    

    On this stage my DotNetCore process hangs on Executing JsonResult, writing value ... and never come. In my Startup.cs, I've already set the JsonOption. For some reason EFCore is including nested property which I'm not asking Ef to give.

        options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    

    expected behavior should be this

    Hey EfCore can you please include "CompanyUsers" data as well in my Company class so that i can easily access the data.

    then

    Hey EfCore can you also please include the "CompanyUsers.User" data as well so that i can easily access the data like this Company.CompanyUsers.First().User.DisplayName

    at this stage i should only get this "Company.CompanyUsers.First().User.DisplayName" and it should not give me Company.CompanyUsers.First().User.CompanyUsers which causing the self-referencing issue; Technically it shouldn't give me User.CompanyUsers as CompanyUsers is a navigational property. But, EfCore get very excited and giving me User.CompanyUsers.

    So, I decided to write an extension method for property to be excluded from the object (it's actually not excluding it's just setting the property to null). Not only that it will also work on array properties as well. below is the code I'm also going to export the nuget package for other users (not sure if this even helps someone). Reason is simple because I'm too lazy to write .Select(n => new { n.p1, n.p2}); I just don't want to write select statement to exclude only 1 property!

    This is not the best code (I'll update at some stage) as I have written in a hurry and though this might help someone who wants to exclude (set null) in the object with arrays also.

        public static class PropertyExtensions
    {
        public static void Exclude(this T obj, Expression> expression)
        {
            var visitor = new PropertyVisitor();
            visitor.Visit(expression.Body);
            visitor.Path.Reverse();
            List paths = visitor.Path;
            Action, object> act = null;
    
            int recursiveLevel = 0;
            act = (List vPath, object vObj) =>
            {
    
                // set last propert to null thats what we want to avoid the self-referencing error.
                if (recursiveLevel == vPath.Count - 1)
                {
                    if (vObj == null) throw new ArgumentNullException("Object cannot be null");
    
                    vObj.GetType().GetMethod($"set_{vPath.ElementAt(recursiveLevel).Name}").Invoke(vObj, new object[] { null });
                    return;
                }
    
                var pi = vObj.GetType().GetProperty(vPath.ElementAt(recursiveLevel).Name);
                if (pi == null) return;
                var pv = pi.GetValue(vObj, null);
                if (pi.PropertyType.IsArray || pi.PropertyType.Name.Contains("HashSet`1") || pi.PropertyType.Name.Contains("ICollection`1"))
                {
                    var ele = (IEnumerator)pv.GetType().GetMethod("GetEnumerator").Invoke(pv, null);
    
                    while (ele.MoveNext())
                    {
                        recursiveLevel++;
                        var arrItem = ele.Current;
    
                        act(vPath, arrItem);
    
                        recursiveLevel--;
                    }
    
                    if (recursiveLevel != 0) recursiveLevel--;
                    return;
                }
                else
                {
                    recursiveLevel++;
                    act(vPath, pv);
                }
    
                if (recursiveLevel != 0) recursiveLevel--;
    
            };
    
            // check if the root level propert is array
            if (obj.GetType().IsArray)
            {
                var ele = (IEnumerator)obj.GetType().GetMethod("GetEnumerator").Invoke(obj, null);
                while (ele.MoveNext())
                {
                    recursiveLevel = 0;
                    var arrItem = ele.Current;
    
                    act(paths, arrItem);
                }
            }
            else
            {
                recursiveLevel = 0;
                act(paths, obj);
            }
    
        }
    
        public static T Explode(this T[] obj)
        {
            return obj.FirstOrDefault();
        }
    
        public static T Explode(this ICollection obj)
        {
            return obj.FirstOrDefault();
        }
    }
    

    above extension class will give you the ability to set the property to null to avoid the self-referencing loop even arrays.

    Expression Builder

        internal class PropertyVisitor : ExpressionVisitor
    {
        public readonly List Path = new List();
    
        public Expression Modify(Expression expression)
        {
            return Visit(expression);
        }
    
    
        protected override Expression VisitMember(MemberExpression node)
        {
            if (!(node.Member is PropertyInfo))
            {
                throw new ArgumentException("The path can only contain properties", nameof(node));
            }
    
            Path.Add(node.Member);
            return  base.VisitMember(node);
        }
    }
    

    Usages:

    Model Classes

        public class Person
    {
        public string Name { get; set; }
        public Address AddressDetail { get; set; }
    }
    
    public class Address
    {
        public string Street { get; set; }
        public Country CountryDetail { get; set; }
        public Country[] CountryDetail2 { get; set; }
    }
    
    public class Country
    {
        public string CountryName { get; set; }
        public Person[] CountryDetail { get; set; }
    }
    

    Dummy Data

               var p = new Person
            {
                Name = "Adeel Rizvi",
                AddressDetail = new Address
                {
                    Street = "Sydney",
                    CountryDetail = new Country
                    {
                        CountryName = "AU"
                    }
                }
            };
    
            var p1 = new Person
            {
                Name = "Adeel Rizvi",
                AddressDetail = new Address
                {
                    Street = "Sydney",
                    CountryDetail2 = new Country[]
                    {
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A1" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A2" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A3" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A4" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A5" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A6" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A7" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A8" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A9" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A1" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A2" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A3" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A4" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A5" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A6" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A7" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A8" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
                        new Country{ CountryName = "AU", CountryDetail = new Person[]{ new Person { Name = "A9" }, new Person { Name = "A1" }, new Person { Name = "A1" } } },
    
                    }
                }
            };
    

    Cases:

    Case 1: Exclude only property without any array

    p.Exclude(n => n.AddressDetail.CountryDetail.CountryName);
    

    Case 2: Exclude property with 1 array

    p1.Exclude(n => n.AddressDetail.CountryDetail2.Explode().CountryName);
    

    Case 3: Exclude property with 2 nested array

    p1.Exclude(n => n.AddressDetail.CountryDetail2.Explode().CountryDetail.Explode().Name);
    

    Case 4: EF GetAll Query With Includes

    var query = cs.GetAll("CompanyUsers", "CompanyUsers.User").ToArray();
    query.Exclude(n => n.Explode().CompanyUsers.Explode().User.CompanyUsers);
    return query;
    

    You have notice that Explode() method its also a extension method just for our expression builder to get the property from array property. Whenever there is a array property use the .Explode().YourPropertyToExclude or .Explode().Property1.MyArrayProperty.Explode().MyStupidProperty. Above code helps me to avoid the self-referencing so deep as deep i want. Now i can use GetAll and exclude the property which i don;t want!

    Thank you for reading this big post!

提交回复
热议问题