You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When an aggregate over a grouping reaches through a required navigation whose principal has a query filter, the group is silently removed from the results — including its Count(), which never touched the navigation.
Lifting the aggregate into a pre-GroupBy join (#38668) moved the navigation from a correlated subquery in the projection to an INNER JOIN beneath the GROUP BY. A correlated subquery cannot remove a source row; it returns NULL for it. An INNER JOIN onto a filtered subquery can, and does — so a row whose principal is filtered out no longer reaches the grouping, and if that was the group's only row, the group itself never forms.
Northwind with the natural soft-delete filter on products:
Order 10248 has one line, for a live product. Order 10249 has one line, for a discontinued one.
// 1 — Count alonectx.OrderDetails.GroupBy(od =>od.OrderID).Select(g =>new{g.Key,Count=g.Count()})// 2 — Count plus an aggregate that traverses Product
ctx.OrderDetails.GroupBy(od =>od.OrderID).Select(g =>new{g.Key,Count=g.Count(),MaxStock=g.Max(x =>(int?)x.Product.UnitsInStock)})// 3 — the same question, traversal moved into the GroupBy element selector
ctx.OrderDetails.GroupBy(od =>od.OrderID, od =>new{od.Product.UnitsInStock,od.Quantity}).Select(g =>new{g.Key,Count=g.Count(),MaxStock=g.Max(x =>(int?)x.UnitsInStock)})
2 groups — 10248:Count=1,Max=39, 10249:Count=1,Max=null
1 group — 10248:Count=1,Max=39; order 10249 is gone
3
2 groups — same as 10.0 query 2
2 groups — unchanged
Two things stand out beyond the regression itself.
Count changes even though it traverses nothing. Queries 1 and 2 differ only by the presence of a second, unrelated aggregate, and adding it removes a group from the result. Whether order 10249 appears is decided by what a sibling aggregate in the same projection does.
Queries 2 and 3 ask the same question and now disagree. The only difference is whether od.Product is reached from the aggregate lambda or from the element selector, which selects a different translation. Before #38668 they agreed.
Expected behaviour
Query 2 returns both orders, with MaxStock = null for order 10249 — the 10.0 behaviour, and what query 3 still returns today. An aggregate materializes no Product; it reads UnitsInStock and folds it. A filtered-out principal should make the aggregate empty, not delete the order.
SQL
Before #38668 — the traversal is isolated in a correlated subquery, so COUNT(*) sees every line:
11.0 rc1 / main — the filter is now a join condition beneath the GROUP BY, so it removes rows from the grouping:
SELECT"o"."OrderID"AS"Key", COUNT(*) AS"Count", MAX("p"."UnitsInStock") AS"MaxStock"FROM"OrderDetails"AS"o"INNER JOIN"Products"AS"p"ON"o"."ProductID"="p"."ProductID"AND NOT ("p"."Discontinued")
GROUP BY"o"."OrderID"
I'm aware that removing a dependent whose required principal is filtered out is deliberate, and that PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning exists for exactly this model. I don't think that covers this case:
It doesn't explain why Count(), which never traverses the navigation, changes its answer.
It doesn't explain why queries 2 and 3 disagree, nor why query 2's behaviour changed in 11.0 while query 3's did not.
The suggested remedy — matching filters on both entities — changes the intent, since it also removes those lines from Count. It cannot express "count every line, and report the stock figure as unavailable", which is what 10.0 returned.
Bug description
When an aggregate over a grouping reaches through a required navigation whose principal has a query filter, the group is silently removed from the results — including its
Count(), which never touched the navigation.Lifting the aggregate into a pre-
GroupByjoin (#38668) moved the navigation from a correlated subquery in the projection to anINNER JOINbeneath theGROUP BY. A correlated subquery cannot remove a source row; it returnsNULLfor it. AnINNER JOINonto a filtered subquery can, and does — so a row whose principal is filtered out no longer reaches the grouping, and if that was the group's only row, the group itself never forms.Northwind with the natural soft-delete filter on products:
Order 10248 has one line, for a live product. Order 10249 has one line, for a discontinued one.
10248:Count=1,10249:Count=110248:Count=1,Max=39,10249:Count=1,Max=null10248:Count=1,Max=39; order 10249 is goneTwo things stand out beyond the regression itself.
Countchanges even though it traverses nothing. Queries 1 and 2 differ only by the presence of a second, unrelated aggregate, and adding it removes a group from the result. Whether order 10249 appears is decided by what a sibling aggregate in the same projection does.Queries 2 and 3 ask the same question and now disagree. The only difference is whether
od.Productis reached from the aggregate lambda or from the element selector, which selects a different translation. Before #38668 they agreed.Expected behaviour
Query 2 returns both orders, with
MaxStock = nullfor order 10249 — the 10.0 behaviour, and what query 3 still returns today. An aggregate materializes noProduct; it readsUnitsInStockand folds it. A filtered-out principal should make the aggregate empty, not delete the order.SQL
Before #38668 — the traversal is isolated in a correlated subquery, so
COUNT(*)sees every line:11.0 rc1 / main — the filter is now a join condition beneath the
GROUP BY, so it removes rows from the grouping:The same plan shape is generated on SQL Server.
Relationship to #19801
I'm aware that removing a dependent whose required principal is filtered out is deliberate, and that
PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarningexists for exactly this model. I don't think that covers this case:Count(), which never traverses the navigation, changes its answer.Count. It cannot express "count every line, and report the stock figure as unavailable", which is what 10.0 returned.Your code
Stack traces
Verbose output
EF Core version
11.0.0-rc.1
Database provider
No response
Target framework
No response
Operating system
No response
IDE
No response