Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework: Querying Child Entities [duplicate]

It seems that I can't get a parent and a subset of its children from the db.

For example...

db.Parents
.Include(p => p.Children)
.Where(p => p.Children.Any(c => c.Age >= 5))

This will return all Parents that have a child aged 5+, but if I iterate through the Parents.Children collection, all children will be present (not just those over 5 years old).

Now the query does make sense to me (I've asked to include children and I've got them!), but can imagine that I would like to have the where clause applied to the child collection in some scenarios.

How could I get an IEnumerable, in which each of the parents has a filtered collection of Children (Age>=5)?

like image 930
ETFairfax Avatar asked Oct 13 '11 10:10

ETFairfax


4 Answers

The only way to get a collection of parents with a filtered children collection in a single database roundtrip is using a projection. It is not possible to use eager loading (Include) because it doesn't support filtering, Include always loads the whole collection. The explicite loading way shown by @Daz requires one roundtrip per parent entity.

Example:

var result = db.Parents
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();

You can directly work with this collection of anonymous type objects. (You can also project into your own named type instead of an anonymous projection (but not into an entity like Parent).)

EF's context will also populate the Children collection of the Parent automatically if you don't disable change tracking (using AsNoTracking() for example). In this case you can then project the parent out of the anonymous result type (happens in memory, no DB query):

var parents = result.Select(a => a.Parent).ToList();

parents[i].Children will contain your filtered children for each Parent.


Edit to your last Edit in the question:

I am after a) A list of parents who have a child older than 5 (and include only those children).

The code above would return all parents and include only the children with Age >= 5, so potentially also parents with an empty children collection if there are only children with Age < 5. You can filter these out using an additional Where clause for the parents to get only the parents which have at least one (Any) child with Age >= 5:

var result = db.Parents
    .Where(p => p.Children.Any(c => c.Age >= 5))
    .Select(p => new
    {
        Parent = p,
        Children = p.Children.Where(c => c.Age >= 5)
    })
    .ToList();
like image 109
Slauma Avatar answered Nov 07 '22 15:11

Slauma


Taking your example the following should do what you need. Take a look here for more info.

db.Entry(Parents)
.Collection("Children")
.Query().Cast<Child>()
.Where(c => c.Age >= 5))
.Load();
like image 38
Darren Lewis Avatar answered Nov 07 '22 14:11

Darren Lewis


In EF Core 5.0, the Include method now supports filtering of the entities included.

https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#filtered-include

var data = db.Parents
    .Include(p => p.Children.Where(c => c.Age >= 5))
    .ToList();
like image 3
Prince Prasad Avatar answered Nov 07 '22 14:11

Prince Prasad


I think parents and child are not really well suited as separate entities. A child can always also be a parent and usually a child has two parents (a father and a mother), so it's not the simplest context. But I assume you just have a simple 1:n relationship as in the following master-slave model that I used.

What you need to do is make a left outer join (that answer has led me on the right path). Such a join is a bit tricky to do, but here's the code

var query = from m in ctx.Masters
            join s in ctx.Slaves
              on m.MasterId equals s.MasterId into masterSlaves
            from ms in masterSlaves.Where(x => x.Age > 5).DefaultIfEmpty()
            select new {
              Master = m,
              Slave = ms
            };

foreach (var item in query) {
  if (item.Slave == null) Console.WriteLine("{0} owns nobody.", item.Master.Name);
  else Console.WriteLine("{0} owns {1} at age {2}.", item.Master.Name, item.Slave.Name, item.Slave.Age);
}

This will translate to the following SQL statement with EF 4.1

SELECT 
[Extent1].[MasterId] AS [MasterId], 
[Extent1].[Name] AS [Name], 
[Extent2].[SlaveId] AS [SlaveId], 
[Extent2].[MasterId] AS [MasterId1], 
[Extent2].[Name] AS [Name1], 
[Extent2].[Age] AS [Age]
FROM  [dbo].[Master] AS [Extent1]
LEFT OUTER JOIN [dbo].[Slave] AS [Extent2]
ON ([Extent1].[MasterId] = [Extent2].[MasterId]) AND ([Extent2].[Age] > 5)

Note that it is important to perform the additional where clause on the age on the joined collection and not between the from and the select.

EDIT:

IF you want a hierarchical result you can convert the flat list by performing a grouping:

var hierarchical = from line in query
                   group line by line.Master into grouped
                   select new { Master = grouped.Key, Slaves = grouped.Select(x => x.Slave).Where(x => x != null) };

foreach (var elem in hierarchical) {
   Master master = elem.Master;
   Console.WriteLine("{0}:", master.Name);
   foreach (var s in elem.Slaves) // note that it says elem.Slaves not master.Slaves here!
     Console.WriteLine("{0} at {1}", s.Name, s.Age);
}

Note that I used an anonymous type to store the hierarchical result. You can of course create also a specific type like this

class FilteredResult {
  public Master Master { get; set; }
  public IEnumerable<Slave> Slaves { get; set; }
}

and then project the group into instances of this class. That makes it easier if you need to pass these results to other methods.

like image 2
Andreas Avatar answered Nov 07 '22 14:11

Andreas