Bogey Bogey - 1 year ago 123
C# Question

EntityFramework Core: Eager loading navigation properties of derived types

I'm using EntityFramework Core, and am trying to eager-load navigation properties that only exist on some derived type (all within a single query).
Probably best to demonstrate with a simple example.

Assume you have some data structure like

class Transaction
public Product product { get; set; }
public DateTime date { get; set; }

abstract class Product
public string Name { get; set; }

class PhysicalProduct : Product
public Photo photo { get; set; }

class Service : Product
public Person provider { get; set; }

And some DbContext

class MyContext : DbContext
public DbSet<Transaction> Transactions;

How can I query MyContext.Transactions to return all transactions, and include (eager load) (in case product is PhysicalProduct), and Transaction.product.provider (in case product is Service) ?
As mentioned, trying to achieve this with only one query.

I've tried the following:

// This is conceptually what I want to achieve.
// Not very surprisingly, this will throw an InvalidCastException
.Include(x => ((PhysicalProduct)x.product).photo)
.Include(x => ((Service)x.product).provider)

// Based on
// Projection into an anonymous type, then transform back.
// doesn't work though, throws an InvalidOperationException, e.g.
// The property "photo" on entity type "Product" could not be found. Ensure that the property exists and has been included in the model.
// i.e. even though I wrapped this in a condition (x.product is PhysicalProduct), seems like EntityFramework still tries to execute or parse the statement thereafter even if the condition is not true.
var query = Transactions.Select(x => new
_transaction = x,
_physicalProductPhoto = (x.product is PhysicalProduct) ? ((PhysicalProduct)x.product).photo : null;
_serviceProvider = (x.product is Service) ? ((Service)x.product).provider : null;
.ToList() // Execute query. Exception will be thrown at this step.
.Select(x =>
var result = x._transaction;

if (x.product is PhysicalProduct)
((PhysicalProduct)x.product).photo = x._physicalProductPhoto;
else if(x.product is Service)
((Service)x.product).provider = x._serviceProvider;

return result;

Can anyone think of a way to achieve this?

Answer Source

Yesterday I was fighting similar issue in EF6 - EF Eager fetching derived class. Currently EF Core is not better in that regard - in fact it's worse, because from the 3 EF6 workarounds only the #2 works here.

The workaround is:

It definitely can't be done with a single query. You need to execute the master query and materialize the result in memory. Then for each derived navigation type, collect the PKs and execute a query filtered by these keys. At the end, due to EF navigation property fixup you'll end up with all the navigation properties loaded.

var transactions = db.Transactions.Include(e => e.product).ToList();

var productIds = transactions.Where(e => e.product is PhysicalProduct)
    .Select(e => e.product.Id).Distinct();
db.BaseProducts.OfType<PhysicalProduct>().Include(e =>
    .Where(e => productIds.Contains(e.Id)).Load();

var serviceIds = transactions.Where(e => e.product is Service)
    .Select(e => e.product.Id).Distinct();
db.BaseProducts.OfType<Service>().Include(e => e.provider)
    .Where(e => serviceIds.Contains(e.Id)).Load();