Skip to content

Fix .OrderBy/.NavigateTo truncating nested (dotted) property paths#37

Merged
rolandbanks merged 1 commit into
mainfrom
fix/orderby-navigateto-nested-path-truncation
Jul 8, 2026
Merged

Fix .OrderBy/.NavigateTo truncating nested (dotted) property paths#37
rolandbanks merged 1 commit into
mainfrom
fix/orderby-navigateto-nested-path-truncation

Conversation

@rolandbanks

Copy link
Copy Markdown
Contributor

Summary

.OrderBy(p => p.Nav.Prop) and .NavigateTo(p => p.Nav.Prop) silently resolved only the leaf property name, dropping any intermediate navigation properties:

.OrderBy(p => p.BestFriend.FirstName)   // produced $orderby=FirstName, not $orderby=BestFriend/FirstName
.NavigateTo(x => x.BestFriend.Friends)  // produced .../Friends, not .../BestFriend/Friends

This was one of four pre-existing truncation bugs surfaced (and deliberately preserved, not fixed) while consolidating member-path resolution in #36. It's fixed here on its own because it's the one of the four that's unambiguously safe: OData v4's $orderby/$filter/resource-path grammar formally supports /-paths through navigation properties, so the corrected output is fully spec-compliant.

The other three (.Select, .ExpandWithSelect, NestedExpandBuilder.Select) are not touched here - fixing those the same way would produce $select paths through navigation properties, which OData v4's $select grammar only defines for complex-typed properties, not navigation properties. That's a separate design question (ship anyway vs. validate-and-throw vs. defer), deliberately left open.

Change

  • MemberPathResolver: new TryGetPathLoose, mirroring the existing TryGetLeafMemberNameLoose's loose Unary-gate but resolving the full path via the already-correct GetFlatPath instead of just the leaf name.
  • GetMemberName (feeds both OrderBy and NavigateTo): swapped to call the new method.
  • That's the entire production diff - one new method, one changed call.

Tests

  • Two existing tests that pinned the buggy truncated output are renamed and flipped to assert the fixed full-path output (OrderBy_WithNestedExpression_ResolvesFullPath, NavigateTo_NonGenericExprNested_ResolvesFullPath).
  • One new 3-level-nesting regression test (OrderBy_WithThreeLevelNestedExpression_ResolvesFullPath).
  • The three deferred $select-family characterization tests are untouched, still green, still pinning their bugs.
  • Full suite: 787/787 passing. Clean Release build, zero warnings.

Changelog

Added a [10.0.88] - Fixed entry.

🤖 Generated with Claude Code

GetMemberName (feeding both .OrderBy(Expression<Func<T,object?>>, bool)
and .NavigateTo(Expression<Func<T,object?>>)) only read the selector
body's immediate leaf member name, silently dropping any intermediate
navigation properties - .OrderBy(p => p.BestFriend.FirstName) produced
$orderby=FirstName instead of $orderby=BestFriend/FirstName.

Adds MemberPathResolver.TryGetPathLoose, mirroring the existing
TryGetLeafMemberNameLoose's loose Unary-gate but resolving the full
path via the already-correct GetFlatPath instead of just the leaf name.
OData v4's $orderby/$filter/resource-path grammar formally supports
"/"-paths through navigation properties, so this fix is fully
spec-compliant.

The three related $select-family truncation bugs (.Select,
.ExpandWithSelect, NestedExpandBuilder.Select) are NOT touched here -
fixing those the same way would produce $select paths through
navigation properties, which OData v4's $select grammar only defines
for complex-typed properties. That's a separate design decision,
deliberately deferred.

Full suite green (787/787).
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 4 complexity · 0 duplication

Metric Results
Complexity 4
Duplication 0

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@rolandbanks
rolandbanks merged commit 4a439de into main Jul 8, 2026
3 checks passed

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a bug where .OrderBy() and .NavigateTo() truncated nested property paths to the leaf member name. The implementation correctly transitions to slash-separated OData paths (e.g., 'Nav/Prop') and handles complex expression structures including multi-level nesting and UnaryExpressions.

Codacy analysis confirms the changes are up to standards. All required test scenarios for path resolution were found in the provided test updates. While the core logic is correct, there are opportunities to improve code maintainability by renaming methods to reflect their updated behavior and consolidating duplicated expression unwrapping logic.

Test suggestions

  • OrderBy with a 2-level nested expression (p => p.BestFriend.FirstName)
  • OrderBy with a 3-level nested expression (p => p.A.B.C)
  • NavigateTo with a nested expression (x => x.BestFriend.Friends)
  • Path resolution for expressions wrapped in UnaryExpressions

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

/// reason that method is kept distinct from <see cref="GetLeafMemberNameOrEmpty"/> - the
/// gates coincide today but must not be silently unified.
/// </summary>
internal static bool TryGetPathLoose(Expression expression, out string path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Suggestion: This method duplicates the expression unwrapping logic (handling MemberExpression and UnaryExpression) found in TryGetLeafMemberNameLoose. Consider consolidating this logic into a shared helper method to improve maintainability.

Try running the following prompt in your IDE agent:

Refactor MemberPathResolver.cs to introduce a private static method TryUnwrapMemberLoose(Expression expression, out MemberExpression member) that encapsulates the logic for extracting a MemberExpression from both direct member access and unary-wrapped expressions. Update TryGetLeafMemberNameLoose and TryGetPathLoose to use this new helper.

@@ -509,7 +509,7 @@ private static string GetMemberPathFromExpression(Expression expression)
}

private static string GetMemberName(Expression<Func<T, object?>> selector) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Suggestion: This method now resolves full navigation paths rather than just the leaf member name. Rename it to GetMemberPath and update the internal name variable to path to better reflect the behavior and maintain consistency with naming conventions in the codebase.

Try running the following prompt in your IDE agent:

Rename the private static method GetMemberName to GetMemberPath in PanoramicData.OData.Client/ODataQueryBuilder.ExpressionParsing.cs, update the internal variable name to path, and update all internal calls to it.

@rolandbanks
rolandbanks deleted the fix/orderby-navigateto-nested-path-truncation branch July 8, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant