TLDR: I would like to know if using different "include logics" for one entity type in a single query possible in EF core.

Edit: Just to be clear, in the title I said keeping track of entities because I think that's what EF does, but .AsNoTracking() does nothing useful here, before anyone suggests.

The problem at hand is on a relatively small React application backed with an ASP.NET Core web api application. What I want to accomplish is, when I call api/parents, I want the application to give me a json that looks like this:

[
  {
    "id": "parentid1",
    "extraProperty": "value",
    "children": [
      {
        "id": "childid1",
        "parent": {
          "id": "parentid1",
          "extraProperty": "value"
        }
      }
    ]
  }
]

My setup looks like this:

EF query:

(from p in _context.Parents
 select p)
 .Include(p => p.Children)
     .ThenInclude(c => c.Parent)
 .ToList();

After that I have AutoMapper mapping the entity to the dto, there is not much going on there. I also use the default Json Serializer (Newtonsoft) for the application. It is configured with SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore.

With this setup the api call gives back this response:

[
  {
    "id": "parentid1",
    "extraProperty": "value",
    "children": [
      {
        "id": "childid1"
      }
    ]
  }
]

Which as you can see "ignores" the self reference of the parent.

The solution I came up with was, I should configure Newtonsoft to "serialize" reference loops, and I tried it. And it throws, because the EF query provided above returns an entity list that looks like this:

[
  {
    "id": "parentid1",
    "extraProperty": "value",
    "children": [
      {
        "id": "childid1",
        "parent": {
          "id": "parentid1",
          "extraProperty": "value",
          "children": [
            {
              "id": "childid1",
              "parent": {
                "id": "parentid1",
                "extraProperty": "value"
                ...
              }
            }
          ]
        }
      }
    ]
  }
]

If you look at my Include calls, it explicitly says that for my child objects I only want the parent object and no other references inside that parent object.

To my best guess, EF uses the initial setup .Include(child).ThenInclude(parent) when it encounters with any Parent object for that query. So when it finds a Parent object in the Child object, instead of using no includes (which I don't have any after the ThenInclude) it uses .Include(child).ThenInclude(parent).

I don't want to solve this problem through hacks with either the mapper or the serializer if I'm not have to. I would like to know if what I am seeking is possible: To use different "include logics" for one entity type in a single query.

2 Answers

1
Moho On

The ThenInclude(parent) call is redundant since you're starting with the parent - the materialized children will already have their Parent property populated. You would need to customize the serialization as @IvanStoev stated in the first comment.

The alternative is to query for children by parent then project the result you want:

var parents = _context.Children
    .Include( c => c.Parent )
    .GroupBy( c => c.Parent )
    .ToArray()
    .Select( g => 
        {
            // assign children to g.Key (the parent object)
            g.Key.Children = g.ToArray(); // I don't know type of `Children` property
            // select parent
            return g.Key;
        } );
0
welrocken On

I decided to solve the problem at the front end of things because other solutions just didn't work for me. For future references I will be posting my journey:

EF

At first it seemed logical to do the trick at data access layer, because I simply missed a very basic thing. I realized it wasn't logical to go that way after the comments. Basically, when you get entities from EF and your objects have references, everytime you have an object more than once, they are the same object (by reference, not value). So expecting them to have different data in them (in my case one of them having some details and the other not) is not logical.

After that I thought maybe I could solve this at mapping stage. I tried doing different Profiles/Configurations for different scenarios and it got very ugly. Then I thought maybe using a single Profile and doing some AfterMap() logic would work (initially loading all the data and then stripping out the unwanted data). But then, unless you do some ugly stuff the same principle applies. AutoMapper also keeps references, so when you modify child.Parent object, the original Parent also gets modified. I could have maybe use cloning and maybe done some other tricks but like I said it gets ugly in my opinion.

For now I'm doing the task manually on the React app. When I fetch the data from the server, I just do

parent.children.forEach(c => c.parent = parent);

Keep in mind normally parent.children.parent is null. This is not the case for entity objects or DTOs, it is just the case after it is serialized. Because I have ReferenceLoopHandling.Ignore set for the serializer.

This fixes all my problems for the application except that my api seems incomplete. The returned JSON looks like it is missing something (maybe it's just me, IDK).

At the end of the day, I am considering having multiple DTOs of the same object type for different scenarios or having id fields returned in the api for completeness' sake in the long run.

Thanks all for the comments and replies.