From 41a38f2e814730bf1169b9340edb8fa84af6ccd8 Mon Sep 17 00:00:00 2001 From: Gavin Barron Date: Thu, 12 Nov 2026 14:02:58 -0800 Subject: [PATCH] fix: restore orderby fallback for unresolved navigation targets Fall back to navigation-property sort metadata only when the annotation target cannot be resolved. Preserve explicit path-level sorting prohibitions. Fixes #952 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../NavigationPropertyGetOperationHandler.cs | 9 +- ...igationPropertyGetOperationHandlerTests.cs | 98 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyGetOperationHandler.cs index 9d0256782..a24ed075a 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyGetOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyGetOperationHandler.cs @@ -178,11 +178,12 @@ protected override void SetParameters(OpenApiOperation operation) AddParameterIfExists(parametersToAdd, Context.CreateFilter, Context.CreateFilter); AddParameterIfExists(parametersToAdd, Context.CreateCount, Context.CreateCount); - var orderByParameter = (string.IsNullOrEmpty(TargetPath), NavigationProperty) switch + // Fall back only for unresolved targets, not for explicit sorting restrictions. + var orderByParameter = (string.IsNullOrEmpty(TargetPath) ? null : Context.Model.GetTargetPath(TargetPath), NavigationProperty) switch { - (false, not null) when NavigationProperty.ToEntityType() is {} entityType => - Context.CreateOrderBy(TargetPath!, entityType), - (true, not null) => Context.CreateOrderBy(NavigationProperty), + ({} target, not null) when NavigationProperty.ToEntityType() is {} entityType => + Context.CreateOrderBy(target, entityType), + (null, not null) => Context.CreateOrderBy(NavigationProperty), _ => null, }; if (orderByParameter != null) diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs index 318287fd8..6e3ad2918 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Xml.Linq; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; using Microsoft.OpenApi.OData.Edm; using Microsoft.OpenApi.OData.Generator; using Microsoft.OpenApi.OData.PathItem.Tests; @@ -145,6 +147,102 @@ public void CreateNavigationGetOperationViaComposableFunctionReturnsCorrectOpera Assert.Contains(operation.Parameters, x => x.Name == "path"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateNavigationGetOperationWithUnresolvedFunctionTargetUsesPropertySortRestrictions(bool sortable) + { + VerifyNavigationSortRestrictions(viaFunction: true, propertySortable: sortable, pathSortable: null); + } + + [Theory] + [InlineData(true, null)] + [InlineData(true, false)] + [InlineData(false, true)] + public void CreateNavigationGetOperationWithResolvedTargetUsesPathSortRestrictions(bool propertySortable, bool? pathSortable) + { + VerifyNavigationSortRestrictions(viaFunction: false, propertySortable, pathSortable); + } + + private void VerifyNavigationSortRestrictions(bool viaFunction, bool propertySortable, bool? pathSortable) + { + string pathAnnotation = pathSortable.HasValue ? $@" + + + + + + + " : ""; + string csdl = $@" + + + + + + + + + + + + id + + + + + + + + + + + + + + + + + + + {pathAnnotation} + + + "; + IEdmModel model = CsdlReader.Parse(XElement.Parse(csdl).CreateReader()); + IEdmEntitySet tables = model.EntityContainer.FindEntitySet("tables"); + IEdmNavigationProperty columns = tables.EntityType.DeclaredNavigationProperties().Single(); + var path = new ODataPath(new ODataNavigationSourceSegment(tables)); + if (viaFunction) + { + path.Push(new ODataOperationSegment(model.SchemaElements.OfType().Single())); + } + else + { + path.Push(new ODataKeySegment(tables.EntityType)); + } + path.Push(new ODataNavigationPropertySegment(columns)); + + var target = model.GetTargetPath(path.GetTargetPath(model)); + Assert.Equal(viaFunction, target is null); + + var settings = new OpenApiConvertSettings { UseStringArrayForQueryOptionsSchema = false }; + var operation = _operationHandler.CreateOperation(new ODataContext(model, settings), path); + + if (viaFunction ? propertySortable : pathSortable ?? true) + { + var orderBy = Assert.Single(operation.Parameters, parameter => parameter.Name == "$orderby"); + var expectedProperties = viaFunction + ? new[] { "name", "name desc" } + : new[] { "id", "id desc", "name", "name desc" }; + Assert.Equal(expectedProperties, orderBy.Schema.Items.Enum.Select(value => value.GetValue())); + } + else + { + Assert.DoesNotContain(operation.Parameters, parameter => parameter.Name == "$orderby"); + } + } + [Fact] public void CreateNavigationGetOperationViaOverloadedComposableFunctionReturnsCorrectOperation() {