1

In one of my applications I'm using EF Core 3.1 with lazy-loading proxies enabled. According to the documentation here and this StackOverflow answer, it seems like I should be able to explicitly load a collection and have that collection marked as loaded if I do something like:

await context.Entry(parent)
    .Collection(p => p.Children)
    .Query()
    .Include(c => c.Grandchildren)
    .Where(p => p.Id > 100)
    .LoadAsync();

This should fetch a filtered subset of the child entities for the parent entity and pull back a collection of related entities on the children themselves.

At that point, I would expect that parent.Children is considered loaded by EF Core, such that I can access that collection later in my code and it won't try to go back to my database i.e. won't try to lazy-load it.

In my application I'm finding that that's not at all the case. In the screenshot below, you can see that the Order collection on the productConsultant entity isn't marked as loaded after I try to do something similar:

I can see it goes to the database and performs close to the query I specify (it includes the "where" condition but doesn't join on to the related entities):

SELECT [o].[Id],
       [o].[Assignee],
       [o].[CommissionAuthorisedInPeriod],
       [o].[CommissionPaidInPeriod],
       [o].[CsiMaxScore],
       [o].[CsiScore],
       [o].[CustomerName],
       [o].[DateDelivered],
       [o].[DateSigned],
       [o].[DeliveredInPeriod],
       [o].[EmailStatus],
       [o].[HasZeroCsiScore],
       [o].[IsFleetOrder],
       [o].[IsSubPrime],
       [o].[OrderStreamCheckpoint],
       [o].[PaymentMethod],
       [o].[Status],
       [t].[Id],
       [t].[Vehicle],
       [t].[VehicleSaleType]
FROM [Order] AS [o]
LEFT JOIN
(
    SELECT [o0].[Id],
           [o0].[Vehicle],
           [o0].[VehicleSaleType]
    FROM [Order] AS [o0]
    WHERE [o0].[VehicleSaleType] IS NOT NULL
          AND [o0].[Vehicle] IS NOT NULL
) AS [t] ON [o].[Id] = [t].[Id]
WHERE ([o].[Assignee] = @__p_0)
      AND ([o].[CommissionAuthorisedInPeriod] = @__activePeriod_1);

However, because it doesn't mark the collection as loaded, when I try to reference the Order collection within the ProductConsultant class, it goes off and does another database query for all the orders the product consultant has ever delivered, even though I've tried explicitly loading the order collection earlier with only a subset of orders:

public void PayCommissions(Period period)
{
    var payableOrders = from o in Orders
                        where o.CommissionAuthorisedInPeriod == period
                        where !o.HasBeenPaid
                        select o;
    // ...
}
SELECT [o].[Id],
       [o].[Assignee],
       [o].[CommissionAuthorisedInPeriod],
       [o].[CommissionPaidInPeriod],
       [o].[CsiMaxScore],
       [o].[CsiScore],
       [o].[CustomerName],
       [o].[DateDelivered],
       [o].[DateSigned],
       [o].[DeliveredInPeriod],
       [o].[EmailStatus],
       [o].[HasZeroCsiScore],
       [o].[IsFleetOrder],
       [o].[IsSubPrime],
       [o].[OrderStreamCheckpoint],
       [o].[PaymentMethod],
       [o].[Status],
       [t].[Id],
       [t].[Vehicle],
       [t].[VehicleSaleType]
FROM [Order] AS [o]
LEFT JOIN
(
    SELECT [o0].[Id],
           [o0].[Vehicle],
           [o0].[VehicleSaleType]
    FROM [Order] AS [o0]
    WHERE [o0].[VehicleSaleType] IS NOT NULL
          AND [o0].[Vehicle] IS NOT NULL
) AS [t] ON [o].[Id] = [t].[Id]
WHERE [o].[Assignee] = @__p_0;

If I were to change the original query such that it didn't do anything but load the collection, then it does mark the collection as loaded, but this is pointless as it pulls back far too much data and doesn't allow me to include nested entity collections:

Like dozens of other problems I've encountered with EF Core 3, this is something that could probably be easily resolved with EF Core 5 (using filtered includes). Until I'm able to upgrade this project to target .NET 5, Is there a way to get this to work with EF Core 3? Is there something I'm missing?

Update

Based on Stephen's answer, I've made the changes shown below. This allows me to fetch back only the relevant data from the database, and prevent EF Core from doing the second query later on:

Tagc
  • 8,736
  • 7
  • 61
  • 114

2 Answers2

3

The short of it is that EF Core's IsLoaded property is true if and only if it can make certain that all related entities are loaded. Projecting after the query invalidates this guarantee.

IsLoaded

true if all the related entities are loaded or the IsLoaded has been explicitly set to true

A potential workaround is to explicitly set this property to tell EF that in this context, you know better, and to not issue queries to reload the entities.

Stephen Vernyi
  • 768
  • 5
  • 11
  • Yep, this works as a decent work-around until I can use filtered includes with EF Core 5. – Tagc Feb 02 '21 at 18:46
  • Not totally accurate. The [documentation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.collectionentry.query?view=efcore-5.0) says: "Returns the query that would be used by Load() to load entities referenced by this navigation property." That seems to imply that loading the navigation property directly by `Load` is different than just getting the query that would be used for this operation. Indeed, `Collection(p => p.Children).Load()` marks the collection as loaded, just executing the query doesn't. It's no longer related to the parent. – Gert Arnold Feb 02 '21 at 20:13
  • Also, note that a filtered `Include` (in EF5) marks partially loaded collections as loaded. – Gert Arnold Feb 02 '21 at 20:14
1

The reason is cause after you do the .query() then you do a . Include which EF thinks your asking for a new query and throws out your old query filters and thus why it hits the database a second time.

Josh
  • 188
  • 1
  • 12
  • So is `.Load()` or `.LoadAsync()` called after `.Query()` just a fundamentally useless thing to do? And if so, why are people upvoting this answer and claiming that it works for them? https://stackoverflow.com/questions/41529100/ef-core-collection-load-of-a-collection/41535211#41535211 Maybe there was a change in behaviour between EF Core 1 and 3 that affects this. – Tagc Feb 02 '21 at 18:08