Skip to content

Commit 1aedc08

Browse files
gavinbarronCopilot
andcommitted
fix: TryAddPath exact-match check for duplicate bound operation paths
When a duplicate path is detected in TryAddPath, the existing heuristic checks if the new operation's binding type has derived types and, if so, keeps the first entry assuming it was more specific. This is incorrect when the first entry came from a base type (e.g., directoryObject) and the new entry is the exact match for the entity set (e.g., servicePrincipal). Add an exact-match check: if the operation's binding type matches the entity set type, always replace the existing entry regardless of derived types. This ensures paths like /servicePrincipals/delta() return the correct type (servicePrincipal) instead of the base type (directoryObject). Fixes #808 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 052e168 commit 1aedc08

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,13 @@ internal static bool TryAddPath(this IDictionary<string, OpenApiPathItem> pathIt
372372

373373
if (derivedTypes?.Any() ?? false)
374374
{
375+
if (boundEntityType != null && boundEntityType == operationEntityType)
376+
{
377+
// The operation's binding type exactly matches the entity set's type,
378+
// so this is a more specific overload than whatever was added first.
379+
pathItems[pathName] = pathItem;
380+
return true;
381+
}
375382
if (boundEntityType != null && !derivedTypes.Contains(boundEntityType))
376383
{
377384
Debug.WriteLine($"Duplicate paths present but entity type of binding parameter '{operationEntityType}' " +

test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.OData.Edm;
1313
using Microsoft.OData.Edm.Csdl;
1414
using Microsoft.OData.Edm.Validation;
15+
using Microsoft.OpenApi.Models;
1516
using Microsoft.OpenApi.OData.Tests;
1617
using Xunit;
1718

@@ -921,6 +922,76 @@ private static IEdmModel GetNavPropModel(string annotation)
921922
return GetEdmModel(template);
922923
}
923924

925+
[Fact]
926+
public void GetPathsForDerivedTypeDeltaFunctionUsesCorrectReturnType()
927+
{
928+
// Arrange – mirrors the Graph scenario:
929+
// directoryObject (base) has delta with RequiresExplicitBinding
930+
// servicePrincipal (derived) has its own delta
931+
// agentIdentity (derived from servicePrincipal) causes servicePrincipal to have derived types
932+
// Bug: TryAddPath kept the base-type delta for /servicePrincipals/delta() because
933+
// servicePrincipal has derived types.
934+
string csdl = @"<edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
935+
<edmx:DataServices>
936+
<Schema Namespace=""NS"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
937+
<EntityType Name=""directoryObject"">
938+
<Key>
939+
<PropertyRef Name=""id"" />
940+
</Key>
941+
<Property Name=""id"" Type=""Edm.String"" Nullable=""false"" />
942+
</EntityType>
943+
<EntityType Name=""servicePrincipal"" BaseType=""NS.directoryObject"">
944+
<Property Name=""appId"" Type=""Edm.String"" />
945+
</EntityType>
946+
<EntityType Name=""agentIdentity"" BaseType=""NS.servicePrincipal"">
947+
<Property Name=""blueprintId"" Type=""Edm.String"" />
948+
</EntityType>
949+
<Function Name=""delta"" IsBound=""true"">
950+
<Parameter Name=""bindingParameter"" Type=""Collection(NS.directoryObject)"" />
951+
<ReturnType Type=""Collection(NS.directoryObject)"" />
952+
<Annotation Term=""Org.OData.Core.V1.RequiresExplicitBinding"" />
953+
</Function>
954+
<Function Name=""delta"" IsBound=""true"">
955+
<Parameter Name=""bindingParameter"" Type=""Collection(NS.servicePrincipal)"" />
956+
<ReturnType Type=""Collection(NS.servicePrincipal)"" />
957+
</Function>
958+
<EntityContainer Name=""Default"">
959+
<EntitySet Name=""directoryObjects"" EntityType=""NS.directoryObject"" />
960+
<EntitySet Name=""servicePrincipals"" EntityType=""NS.servicePrincipal"" />
961+
</EntityContainer>
962+
<Annotations Target=""NS.directoryObject"">
963+
<Annotation Term=""Org.OData.Core.V1.ExplicitOperationBindings"">
964+
<Collection>
965+
<String>NS.delta</String>
966+
</Collection>
967+
</Annotation>
968+
</Annotations>
969+
</Schema>
970+
</edmx:DataServices>
971+
</edmx:Edmx>";
972+
973+
bool result = CsdlReader.TryParse(XElement.Parse(csdl).CreateReader(), out IEdmModel model, out _);
974+
Assert.True(result);
975+
976+
var settings = new OpenApiConvertSettings();
977+
OpenApiDocument doc = model.ConvertToOpenApi(settings);
978+
979+
// Assert – the /servicePrincipals/NS.delta() path should exist and its response
980+
// should reference NS.servicePrincipal, not NS.directoryObject
981+
Assert.True(doc.Paths.ContainsKey("/servicePrincipals/NS.delta()"),
982+
$"Expected path not found. Available: {string.Join(", ", doc.Paths.Keys)}");
983+
984+
var pathItem = doc.Paths["/servicePrincipals/NS.delta()"];
985+
var getOp = pathItem.Operations[OperationType.Get];
986+
Assert.NotNull(getOp);
987+
988+
// The response schema title should reference servicePrincipal, not directoryObject
989+
var responseKey = getOp.Responses.Keys.First();
990+
var response = getOp.Responses[responseKey];
991+
var schema = response.Content["application/json"].Schema;
992+
Assert.Contains("servicePrincipal", schema.Title);
993+
}
994+
924995
private static IEdmModel GetEdmModel(string schema)
925996
{
926997
bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out IEnumerable<EdmError> errors);

0 commit comments

Comments
 (0)