Skip to content

Fix/8262 orderby props nested selector#9840

Open
Tommsy64 wants to merge 3 commits into
ChilliCream:mainfrom
Tommsy64:fix/8262-orderby-props-nested-selector
Open

Fix/8262 orderby props nested selector#9840
Tommsy64 wants to merge 3 commits into
ChilliCream:mainfrom
Tommsy64:fix/8262-orderby-props-nested-selector

Conversation

@Tommsy64
Copy link
Copy Markdown
Contributor

@Tommsy64 Tommsy64 commented Jun 3, 2026

Fixes #8262 and adds associated tests.

The root cause was the paging expression visitors walking the entire query expression tree and treating every OrderBy/ThenBy they encountered as a pagination concern, including order operations inside lambda bodies.

The first commit addresses this issue in the four location this occurs by overriding VisitLambda. The second commit moves the duplicated override into a shared base class.

@Tommsy64
Copy link
Copy Markdown
Contributor Author

Tommsy64 commented Jun 3, 2026

I added a third commit that fixes a similar case when a sort key itself contains a nested OrderBy:

Sorting: new SortDefinition<Brand>()
    .AddAscending(t => t.Products.OrderBy(p => p.Price).First().Price)
    .AddAscending(t => t.Id)

The visitor now tracks the key lambda's parameter and collects only members rooted directly at it. I'm sure queries could still be constructed that break the paging expression visitors, but this fix is reasonably scoped and covers the common cases.

@Tommsy64 Tommsy64 marked this pull request as ready for review June 3, 2026 22:33
Copilot AI review requested due to automatic review settings June 3, 2026 22:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds regression coverage and refactors pagination-related expression visitors to avoid traversing nested lambdas (e.g., OrderBy inside projections/predicates), preventing accidental hoisting/collection of non-pagination order keys during cursor generation.

Changes:

  • Added new paging regression tests plus snapshots for NET8/NET9/NET10 target frameworks.
  • Introduced QueryChainVisitor to restrict visitors to the top-level LINQ method chain (excluding lambda bodies by default).
  • Updated visitors/parsers used by paging (cursor parsing, order reversing, order property extraction, order removal) to derive from QueryChainVisitor.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/PagingHelperTests.cs Adds regression tests for nested OrderBy in selector/predicate/order keys and cursor creation.
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Forward_Paging_NET8_0.md Snapshot for SQL/expression when nested OrderBy exists in selector (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Forward_Paging_NET9_0.md Snapshot for SQL/expression when nested OrderBy exists in selector (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Forward_Paging_NET10_0.md Snapshot for SQL/expression when nested OrderBy exists in selector (NET10).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Does_Not_Hoist_Inner_Order_Properties_NET8_0.md Snapshot verifying inner order properties are not hoisted (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Does_Not_Hoist_Inner_Order_Properties_NET9_0.md Snapshot verifying inner order properties are not hoisted (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Selector_With_Nested_OrderBy_Does_Not_Hoist_Inner_Order_Properties_NET10_0.md Snapshot verifying inner order properties are not hoisted (NET10).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Forward_Paging_NET8_0.md Snapshot for nested OrderBy in predicate forward paging (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Forward_Paging_NET9_0.md Snapshot for nested OrderBy in predicate forward paging (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Forward_Paging_NET10_0.md Snapshot for nested OrderBy in predicate forward paging (NET10).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Backward_Paging_NET8_0.md Snapshot for nested OrderBy in predicate backward paging (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Backward_Paging_NET9_0.md Snapshot for nested OrderBy in predicate backward paging (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_Predicate_With_Nested_OrderBy_Backward_Paging_NET10_0.md Snapshot for nested OrderBy in predicate backward paging (NET10).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Forward_Paging_NET8_0.md Snapshot for nested OrderBy inside sort key forward paging (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Forward_Paging_NET9_0.md Snapshot for nested OrderBy inside sort key forward paging (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Forward_Paging_NET10_0.md Snapshot for nested OrderBy inside sort key forward paging (NET10).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Second_Page_NET8_0.md Snapshot verifying second-page keyset predicate translation (NET8).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Second_Page_NET9_0.md Snapshot verifying second-page keyset predicate translation (NET9).
src/GreenDonut/test/GreenDonut.Data.EntityFramework.Tests/snapshots/PagingHelperTests.QueryContext_OrderKey_With_Nested_OrderBy_Second_Page_NET10_0.md Snapshot verifying second-page keyset predicate translation (NET10).
src/GreenDonut/src/GreenDonut.Data/Expressions/QueryChainVisitor.cs Introduces a base visitor that skips lambda bodies during traversal.
src/GreenDonut/src/GreenDonut.Data/Cursors/CursorKeyParser.cs Switches cursor key parsing visitor to QueryChainVisitor to avoid nested lambda traversal.
src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ReverseOrderExpressionRewriter.cs Switches reverse-order rewriter to QueryChainVisitor so only top-level order ops are rewritten.
src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExtractOrderPropertiesVisitor.cs Refines order property extraction to avoid collecting from nested lambdas/intermediate results.
src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs Updates order-by removal rewriter to rely on chain-only traversal rather than projection state tracking.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 33 to 41
protected override Expression VisitMember(MemberExpression node)
{
if (_isOrderScope)
// an order key is a lambda. we only collect members rooted at that lambda's own
// parameter; members rooted on nested lambdas or intermediate results cannot be
// hoisted into the selector.
if (_orderKeyParameter is not null && node.Expression == _orderKeyParameter)
{
// we only collect members that are within an order method.
OrderProperties.Add(node);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check runs per-node within a recursive traversal: base.VisitMember(node) descends into node.Expression, so for t.Parent.Child.Id the visitor also fires on t.Parent.Child and t.Parent, and then t.Parent matches, since its Expression is the parameter.

Comment on lines +333 to +336
DisplayName = t.Products
.OrderByDescending(p => p.Price)
.ThenBy(p => p.AvailableStock)
.FirstOrDefault()!.Name
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ToPageAsync() (QueryHelpers.EnsureOrderPropsAreSelected) attempts to select Order properties that it should not

2 participants