Skip to content

Commit 8b75828

Browse files
authored
Merge pull request #812 from microsoft/fix/tryaddpath-delta-resolution-v3
fix: TryAddPath exact-match check for duplicate bound operation paths (OpenAPI 3.2)
2 parents 14ddd5d + c49b868 commit 8b75828

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ internal static bool TryAddPath(this IDictionary<string, IOpenApiPathItem> pathI
345345

346346
if (derivedTypes?.Any() ?? false)
347347
{
348+
if (boundEntityType != null && boundEntityType == operationEntityType)
349+
{
350+
// The operation's binding type exactly matches the entity set's type,
351+
// so this is a more specific overload than whatever was added first.
352+
pathItems[pathName] = pathItem;
353+
return true;
354+
}
348355
if (boundEntityType != null && !derivedTypes.Contains(boundEntityType))
349356
{
350357
Debug.WriteLine($"Duplicate paths present but entity type of binding parameter '{operationEntityType}' " +

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
using System.Collections.Generic;
88
using System.IO;
99
using System.Linq;
10+
using System.Threading;
11+
using System.Threading.Tasks;
1012
using System.Xml;
1113
using System.Xml.Linq;
1214
using Microsoft.OData.Edm;
1315
using Microsoft.OData.Edm.Csdl;
1416
using Microsoft.OData.Edm.Validation;
17+
using Microsoft.OpenApi;
1518
using Microsoft.OpenApi.OData.Tests;
1619
using Xunit;
1720

@@ -921,6 +924,83 @@ private static IEdmModel GetNavPropModel(string annotation)
921924
return GetEdmModel(template);
922925
}
923926

927+
[Fact]
928+
public async Task GetPathsForDerivedTypeDeltaFunctionUsesCorrectReturnType()
929+
{
930+
// Arrange – mirrors the Graph scenario:
931+
// directoryObject (base) has delta with RequiresExplicitBinding
932+
// servicePrincipal (derived) has its own delta
933+
// agentIdentity (derived from servicePrincipal) causes servicePrincipal to have derived types
934+
// Bug: TryAddPath kept the base-type delta for /servicePrincipals/delta() because
935+
// servicePrincipal has derived types.
936+
string csdl = @"<edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
937+
<edmx:DataServices>
938+
<Schema Namespace=""NS"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
939+
<EntityType Name=""directoryObject"">
940+
<Key>
941+
<PropertyRef Name=""id"" />
942+
</Key>
943+
<Property Name=""id"" Type=""Edm.String"" Nullable=""false"" />
944+
</EntityType>
945+
<EntityType Name=""servicePrincipal"" BaseType=""NS.directoryObject"">
946+
<Property Name=""appId"" Type=""Edm.String"" />
947+
</EntityType>
948+
<EntityType Name=""agentIdentity"" BaseType=""NS.servicePrincipal"">
949+
<Property Name=""blueprintId"" Type=""Edm.String"" />
950+
</EntityType>
951+
<Function Name=""delta"" IsBound=""true"">
952+
<Parameter Name=""bindingParameter"" Type=""Collection(NS.directoryObject)"" />
953+
<ReturnType Type=""Collection(NS.directoryObject)"" />
954+
<Annotation Term=""Org.OData.Core.V1.RequiresExplicitBinding"" />
955+
</Function>
956+
<Function Name=""delta"" IsBound=""true"">
957+
<Parameter Name=""bindingParameter"" Type=""Collection(NS.servicePrincipal)"" />
958+
<ReturnType Type=""Collection(NS.servicePrincipal)"" />
959+
</Function>
960+
<EntityContainer Name=""Default"">
961+
<EntitySet Name=""directoryObjects"" EntityType=""NS.directoryObject"" />
962+
<EntitySet Name=""servicePrincipals"" EntityType=""NS.servicePrincipal"" />
963+
</EntityContainer>
964+
<Annotations Target=""NS.directoryObject"">
965+
<Annotation Term=""Org.OData.Core.V1.ExplicitOperationBindings"">
966+
<Collection>
967+
<String>NS.delta</String>
968+
</Collection>
969+
</Annotation>
970+
</Annotations>
971+
</Schema>
972+
</edmx:DataServices>
973+
</edmx:Edmx>";
974+
975+
bool result = CsdlReader.TryParse(XElement.Parse(csdl).CreateReader(), out IEdmModel model, out _);
976+
Assert.True(result);
977+
978+
var settings = new OpenApiConvertSettings();
979+
var doc = model.ConvertToOpenApi(settings);
980+
981+
// Serialize to YAML and verify the response type
982+
using var stream = new MemoryStream();
983+
await doc.SerializeAsync(stream, OpenApiSpecVersion.OpenApi3_1, "yaml", CancellationToken.None);
984+
stream.Position = 0;
985+
string yaml = await new StreamReader(stream).ReadToEndAsync();
986+
987+
// The /servicePrincipals/NS.delta() path should reference servicePrincipal, not directoryObject
988+
Assert.Contains("/servicePrincipals/NS.delta()", yaml);
989+
990+
// Extract just the path section (up to 'components:' or next top-level key)
991+
int pathIndex = yaml.IndexOf("/servicePrincipals/NS.delta():");
992+
Assert.True(pathIndex >= 0, "Path /servicePrincipals/NS.delta() not found in YAML output");
993+
994+
int componentsIndex = yaml.IndexOf("\ncomponents:", pathIndex);
995+
string pathSection = componentsIndex > 0
996+
? yaml.Substring(pathIndex, componentsIndex - pathIndex)
997+
: yaml.Substring(pathIndex);
998+
999+
// The response schema items $ref should reference servicePrincipal
1000+
Assert.Contains("'#/components/schemas/NS.servicePrincipal'", pathSection);
1001+
Assert.DoesNotContain("#/components/schemas/NS.directoryObject", pathSection);
1002+
}
1003+
9241004
private static IEdmModel GetEdmModel(string schema)
9251005
{
9261006
bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out IEnumerable<EdmError> errors);

0 commit comments

Comments
 (0)