21

Given the following class structure

 public class Parent 
    {
        public Guid Id { get; 
        public List<BaseChild> Children { get; set; }
    }

 public abstract class BaseChild
    {
        public int Id { get; set; }
        public string ChildName { get; set; }
    }

public class NormalChild : BaseChild
    {
         public DateTime BirthDate { get; set; }
    }

public class RichChild : BaseChild
    {
        public List<OffshoreAccount> OffshoreAccounts { get; set; }
    }

public class OffshoreAccount 
    {
        public string AccountNumber { get; set; }
        public AccountInfo AccountInfo { get; set; }
    }

What is the best way to query parent data to include information about the children's offshore accounts?. I came up with the solution below, using ef-core's explicit loading, but it just doesn't feel right. Is there a more elegant solution?

var parent = Context.Set<Parent>()
    .Where(o => o.Id == Guid.Parse(parentId))
    .Include(o => o.Children)
    .SingleOrDefault();

foreach (var child in parent.Children.OfType<RichChild>())
    {
        Context.Entry<RichChild>(child).Collection(f => f.OffshoreAccounts).Load();
        foreach (var account in child.OffshoreAccounts)
            {
                 Context.Entry<OffshoreAccount>(account).Reference(f => f.AccountInfo).Load();
            }
     }
Elmer Ortega
  • 469
  • 4
  • 12

2 Answers2

20

Update (EF Core 2.1+):

Starting with v2.1, EF Core native supports Include on derived types through C# cast or as operators.

e.g

.Include(e => e.Children)
    .ThenInclude(e => ((RichChild)e).OffshoreAccounts)
        .ThenInclude(e => e.AccountInfo)

or

.Include(e => e.Children)
    .ThenInclude(e => (e as RichChild).OffshoreAccounts)
        .ThenInclude(e => e.AccountInfo)

The documentation claims that the string overload of Include coudld also be used, e.g. according to it

.Include(e => "Children.OffshoreAccounts.AccountInfo")

should also work, but it doesn't (checked up to v3.1.4).

Original:

Currently there is no way to accomplish that in the parent query, but the explicit loading can be improved by using a combination of Entry, Collection, Query, Include / ThenInclude and Load calls:

var parent = Context.Set<Parent>()
    .Where(o => o.Id == Guid.Parse(parentId))
    .Include(o => o.Children)
    .SingleOrDefault();

Context.Entry(parent).Collection(e => e.Children)
    .Query().OfType<RichChild>()
    .Include(e => e.OffshoreAccounts)
        .ThenInclude(e => e.AccountInfo)
    .Load();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • How can I expand this to a further down tph? To give context I have the following structure (working on a form builder): Form.Questions.Control With control being the basetype for classes such as dropdown or TextField. – Peter-Paul Apr 06 '17 at 08:25
  • @Peter-Paul So you want to include parts of Control? I can't say exactly - since the above is some sort of a hack, hence no generically extendable, you might consider posting your own concrete question with sample [mcve]. – Ivan Stoev Apr 06 '17 at 08:33
  • I eventually did it by first getting the form, then looping over the questions to add the class specific properties control by control. Lots of database calls, but it got the job done. – Peter-Paul Apr 06 '17 at 11:51
  • @IvanStoev Have you got a github issue link or doc that mentions this? – Dealdiane May 16 '17 at 09:23
  • 1
    @Dealdiane [#3910](https://github.com/aspnet/EntityFramework/issues/3910) – Ivan Stoev May 16 '17 at 10:27
13

In the current EFCore (2.1.1) you can use type casting in ThenInclude to get the results you're looking for:

var parent = _context.Set<Parent>()
                 .Include(x => x.Children)
                 .ThenInclude(y => (y as RichChild).OffshoreAccounts)
                 .SingleOrDefault();
Marc Brekoo
  • 576
  • 1
  • 10
  • 24