Fix .OrderBy/.NavigateTo truncating nested (dotted) property paths#37
Conversation
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).
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 4 |
| Duplication | 0 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
⚪ 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.csto introduce a private static methodTryUnwrapMemberLoose(Expression expression, out MemberExpression member)that encapsulates the logic for extracting aMemberExpressionfrom both direct member access and unary-wrapped expressions. UpdateTryGetLeafMemberNameLooseandTryGetPathLooseto use this new helper.
| @@ -509,7 +509,7 @@ private static string GetMemberPathFromExpression(Expression expression) | |||
| } | |||
|
|
|||
| private static string GetMemberName(Expression<Func<T, object?>> selector) => | |||
There was a problem hiding this comment.
⚪ 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
GetMemberNametoGetMemberPathinPanoramicData.OData.Client/ODataQueryBuilder.ExpressionParsing.cs, update the internal variablenametopath, and update all internal calls to it.
Summary
.OrderBy(p => p.Nav.Prop)and.NavigateTo(p => p.Nav.Prop)silently resolved only the leaf property name, dropping any intermediate navigation properties: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$selectpaths through navigation properties, which OData v4's$selectgrammar 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: newTryGetPathLoose, mirroring the existingTryGetLeafMemberNameLoose's loose Unary-gate but resolving the full path via the already-correctGetFlatPathinstead of just the leaf name.GetMemberName(feeds bothOrderByandNavigateTo): swapped to call the new method.Tests
OrderBy_WithNestedExpression_ResolvesFullPath,NavigateTo_NonGenericExprNested_ResolvesFullPath).OrderBy_WithThreeLevelNestedExpression_ResolvesFullPath).$select-family characterization tests are untouched, still green, still pinning their bugs.Changelog
Added a
[10.0.88] - Fixedentry.🤖 Generated with Claude Code