21

I have created classes using EF Code First that have collections of each other. Entities:

public class Field
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual List<AppUser> Teachers { get; set; }
    public Field()
    {
        Teachers = new List<AppUser>();
    }
}

public class AppUser
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string UserName => Email;
    public virtual List<Field> Fields { get; set; }
    public AppUser()
    {
        Fields = new List<FieldDTO>();
    }
}

DTOs:

public class FieldDTO
{ 
    public int Id { get; set; }
    public string Name { get; set; }
    public List<AppUserDTO> Teachers { get; set; }
    public FieldDTO()
    {
        Teachers = new List<AppUserDTO>();
    }
}

 public class AppUserDTO
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string UserName => Email;
    public List<FieldDTO> Fields { get; set; }
    public AppUserDTO()
    {
        Fields = new List<FieldDTO>();
    }
}

Mappings:

Mapper.CreateMap<Field, FieldDTO>();
Mapper.CreateMap<FieldDTO, Field>();
Mapper.CreateMap<AppUserDTO, AppUser>();
Mapper.CreateMap<AppUser, AppUserDTO>();

And I am getting StackOverflowException when calling this code (Context is my dbContext):

protected override IQueryable<FieldDTO> GetQueryable()
{
    IQueryable<Field> query = Context.Fields;
    return query.ProjectTo<FieldDTO>();//exception thrown here
}

I guess this happens because it loops in Lists calling each other endlessly. But I do not understand why this happens. Are my mappings wrong?

Peter
  • 449
  • 1
  • 3
  • 13
  • You're right. The problem is an infinite loop when calling mapper on lists. Your mappings are right. You can try to empty lists before convert entities. – erikscandola May 16 '16 at 10:35

5 Answers5

36

You have self-referencing entities AND self-referencing DTOs. Generally speaking self-referencing DTOs are a bad idea. Especially when doing a projection - EF does not know how to join together and join together and join together a hierarchy of items.

You have two choices.

First, you can force a specific depth of hierarchy by explicitly modeling your DTOs with a hierarchy in mind:

public class FieldDTO
{ 
    public int Id { get; set; }
    public string Name { get; set; }
    public List<TeacherDTO> Teachers { get; set; }
    public FieldDTO()
    {
        Teachers = new List<TeacherDTO>();
    }
}

public class TeacherDTO 
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string UserName => Email;
}

public class AppUserDTO : TeacherDTO
{
    public List<FieldDTO> Fields { get; set; }
    public AppUserDTO()
    {
         Fields = new List<FieldDTO>();
    }
}

This is the preferred way, as it's the most obvious and explicit.

The less obvious, less explicit way is to configure AutoMapper to have a maximum depth it will go to traverse hierarchical relationships:

CreateMap<AppUser, AppUserDTO>().MaxDepth(3);

I prefer to go #1 because it's the most easily understood, but #2 works as well.

Mazdak Shojaie
  • 1,620
  • 1
  • 25
  • 32
Jimmy Bogard
  • 26,045
  • 5
  • 74
  • 69
  • Thank you Jimmy. You really helped me out. I think I will go for first choice. It will take some time to refactor whole code but it will be worth it. – Peter May 16 '16 at 16:01
  • @Jimmy Bogard: in the first approach, is the mapping order important? – Mazdak Shojaie Jul 25 '17 at 07:53
  • I find that .MaxDepth(n) is very useful for making the map complete during testing so that I can then dig in to find out where the recursion is coming from for complex graphs. But I always take it out afterwards, so that if the model changes, and recursion is re-introduced by accident, we'll get an error again. – Nich Overend Jan 16 '18 at 19:25
  • Great feature. awesome. – Prasad Kanaparthi May 18 '18 at 07:25
  • 2
    `MaxDepth` doesn't seem to work for me - version `7.0.1.0` – JobaDiniz Apr 28 '20 at 17:19
  • 1
    we were using `MaxDepth` + `PreserveReferences` and oddly using both was the problem. We removed `PreserveReferences` and left only `MaxDepth` and it worked in our case – JobaDiniz Apr 28 '20 at 17:47
12

Other option is using PreserveReferences() method.

CreateMap<AppUser, AppUserDTO>().PreserveReferences();
glanes
  • 149
  • 1
  • 2
0

I use this generic method:

        public static TTarget Convert<TSource, TTarget>(TSource sourceItem)
    {
        if (null == sourceItem)
        {
            return default(TTarget);
        }

        var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };

        var serializedObject = JsonConvert.SerializeObject(sourceItem, deserializeSettings);

        return JsonConvert.DeserializeObject<TTarget>(serializedObject);
    }
Xtremexploit
  • 319
  • 4
  • 7
-1
...
MapperConfiguration(cfg =>
{
    cfg.ForAllMaps((map, exp) => exp.MaxDepth(1));
...
-1

When you giving 1 navigation_property to 2nd entity and visa-versa it go in an infinite loop state. So, the compiler automatically throws a Stackoverflow exception.

So, to avoid that, you just need to remove one navigation_property from any of the entities.