diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs b/src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs
index f4ade9d9..7bd5df68 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs
@@ -93,38 +93,40 @@ internal static bool NavigationRestrictionsAllowsNavigability(
/// Generates the operation id from a navigation property path.
///
/// The target .
+ /// The OData context.
/// Optional: Identifier indicating whether it is a collection-valued non-indexed or single-valued navigation property.
/// The operation id generated from the given navigation property path.
- internal static string GenerateNavigationPropertyPathOperationId(ODataPath path, string prefix = null)
+ internal static string GenerateNavigationPropertyPathOperationId(ODataPath path, ODataContext context, string prefix = null)
{
- IList items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
+ IList items = RetrieveNavigationPropertyPathsOperationIdSegments(path, context);
if (!items.Any())
return null;
- int lastItemIndex = items.Count - 1;
+ int lastItemIndex = items[^1].StartsWith('-') ? items.Count - 2 : items.Count - 1;
if (!string.IsNullOrEmpty(prefix))
{
- items[lastItemIndex] = prefix + Utils.UpperFirstChar(items.Last());
+ items[lastItemIndex] = prefix + Utils.UpperFirstChar(items[lastItemIndex]);
}
else
{
- items[lastItemIndex] = Utils.UpperFirstChar(items.Last());
+ items[lastItemIndex] = Utils.UpperFirstChar(items[lastItemIndex]);
}
- return string.Join(".", items);
+ return GenerateNavigationPropertyPathOperationId(items);
}
///
/// Generates the operation id from a complex property path.
///
/// The target .
+ /// The OData context.
/// Optional: Identifier indicating whether it is a collection-valued or single-valued complex property.
/// The operation id generated from the given complex property path.
- internal static string GenerateComplexPropertyPathOperationId(ODataPath path, string prefix = null)
+ internal static string GenerateComplexPropertyPathOperationId(ODataPath path, ODataContext context, string prefix = null)
{
- IList items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
+ IList items = RetrieveNavigationPropertyPathsOperationIdSegments(path, context);
if (!items.Any())
return null;
@@ -141,15 +143,29 @@ internal static string GenerateComplexPropertyPathOperationId(ODataPath path, st
items.Add(Utils.UpperFirstChar(lastSegment?.Identifier));
}
- return string.Join(".", items);
+ return GenerateNavigationPropertyPathOperationId(items);
+ }
+
+ ///
+ /// Generates a navigation property operation id from a list of string values.
+ ///
+ /// The list of string values.
+ /// The generated navigation property operation id.
+ private static string GenerateNavigationPropertyPathOperationId(IList items)
+ {
+ if (!items.Any())
+ return null;
+
+ return string.Join(".", items).Replace(".-", "-", StringComparison.OrdinalIgnoreCase); // Format any hashed value appropriately (this will be the last value)
}
///
/// Retrieves the segments of an operation id generated from a navigation property path.
///
/// The target .
+ /// The OData context.
/// The segments of an operation id generated from the given navigation property path.
- internal static IList RetrieveNavigationPropertyPathsOperationIdSegments(ODataPath path)
+ internal static IList RetrieveNavigationPropertyPathsOperationIdSegments(ODataPath path, ODataContext context)
{
Utils.CheckArgumentNull(path, nameof(path));
@@ -173,6 +189,8 @@ s is ODataOperationSegment ||
Utils.CheckArgumentNull(segments, nameof(segments));
string previousTypeCastSegmentId = null;
+ string pathHash = string.Empty;
+
foreach (var segment in segments)
{
if (segment is ODataNavigationPropertySegment navPropSegment)
@@ -192,6 +210,14 @@ s is ODataOperationSegment ||
else if (segment is ODataOperationSegment operationSegment)
{
// Navigation property generated via composable function
+ if (operationSegment.Operation is IEdmFunction function && context.Model.IsOperationOverload(function))
+ {
+ // Hash the segment to avoid duplicate operationIds
+ pathHash = string.IsNullOrEmpty(pathHash)
+ ? operationSegment.GetPathHash(context.Settings)
+ : (pathHash + operationSegment.GetPathHash(context.Settings)).GetHashSHA256()[..4];
+ }
+
items.Add(operationSegment.Identifier);
}
else if (segment is ODataKeySegment keySegment && keySegment.IsAlternateKey)
@@ -208,6 +234,11 @@ s is ODataOperationSegment ||
}
}
+ if (!string.IsNullOrEmpty(pathHash))
+ {
+ items.Add("-" + pathHash);
+ }
+
return items;
}
@@ -320,9 +351,10 @@ internal static string GenerateComplexPropertyPathTagName(ODataPath path, ODataC
/// Generates the operation id prefix from an OData type cast path.
///
/// The target .
+ /// The OData context.
/// Optional: Whether to include the List or Get prefix to the generated operation id.
/// The operation id prefix generated from the OData type cast path.
- internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path, bool includeListOrGetPrefix = true)
+ internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path, ODataContext context, bool includeListOrGetPrefix = true)
{
// Get the segment before the last OData type cast segment
ODataTypeCastSegment typeCastSegment = path.Segments.OfType()?.Last();
@@ -352,7 +384,7 @@ internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path
if (secondLastSegment is ODataComplexPropertySegment complexSegment)
{
string listOrGet = includeListOrGetPrefix ? (complexSegment.Property.Type.IsCollection() ? "List" : "Get") : null;
- operationId = GenerateComplexPropertyPathOperationId(path, listOrGet);
+ operationId = GenerateComplexPropertyPathOperationId(path, context, listOrGet);
}
else if (secondLastSegment is ODataNavigationPropertySegment navPropSegment)
{
@@ -362,13 +394,13 @@ internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path
prefix = navPropSegment?.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many ? "List" : "Get";
}
- operationId = GenerateNavigationPropertyPathOperationId(path, prefix);
+ operationId = GenerateNavigationPropertyPathOperationId(path, context, prefix);
}
else if (secondLastSegment is ODataKeySegment keySegment)
{
if (isIndexedCollValuedNavProp)
{
- operationId = GenerateNavigationPropertyPathOperationId(path, "Get");
+ operationId = GenerateNavigationPropertyPathOperationId(path, context, "Get");
}
else
{
diff --git a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj
index d0950ef2..fc5d22a0 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj
+++ b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj
@@ -15,7 +15,7 @@
net8.0
Microsoft.OpenApi.OData
true
- 2.0.0-preview.5
+ 2.0.0-preview.6
This package contains the codes you need to convert OData CSDL to Open API Document of Model.
© Microsoft Corporation. All rights reserved.
Microsoft OpenApi OData EDM
@@ -28,9 +28,10 @@
- Adds nullable to double schema conversions #581
- Updates tag names for actions/functions operations #585
- Creates unique operation ids for paths with composable overloaded functions #580
- - Further fixes for double/decimal/float schema conversions #581
- - Replaced integer types by number types
+ - Further fixes for double/decimal/float schema conversions #581
+ - Replaced integer types by number types
- Further fix for generating unique operation ids for paths with composable overloaded functions where all functions in path are overloaded #594
+ - Further fix for generating unique operation ids for navigation property paths with composable overloaded functions #596
Microsoft.OpenApi.OData.Reader
..\..\tool\Microsoft.OpenApi.OData.snk
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyGetOperationHandler.cs
index 072ac9e8..1f8d3124 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyGetOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyGetOperationHandler.cs
@@ -39,7 +39,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
if (Context.Settings.EnableOperationId)
{
string prefix = ComplexPropertySegment.Property.Type.IsCollection() ? "List" : "Get";
- operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, prefix);
+ operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, prefix);
}
// Summary and Description
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyPostOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyPostOperationHandler.cs
index 50b58e12..6fd736c6 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyPostOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyPostOperationHandler.cs
@@ -42,7 +42,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
{
- operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, "Set");
+ operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, "Set");
}
// Summary and Description
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyUpdateOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyUpdateOperationHandler.cs
index d3790baa..e9a40921 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyUpdateOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/ComplexPropertyUpdateOperationHandler.cs
@@ -40,7 +40,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
{
- operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, "Update");
+ operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, "Update");
}
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/DollarCountGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/DollarCountGetOperationHandler.cs
index c9832820..69b76302 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/DollarCountGetOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/DollarCountGetOperationHandler.cs
@@ -128,17 +128,17 @@ protected override void SetBasicInfo(OpenApiOperation operation)
}
else if (SecondLastSegment is ODataNavigationPropertySegment)
{
- var navPropOpId = string.Join(".", EdmModelHelper.RetrieveNavigationPropertyPathsOperationIdSegments(Path));
+ var navPropOpId = string.Join(".", EdmModelHelper.RetrieveNavigationPropertyPathsOperationIdSegments(Path, Context));
operation.OperationId = $"{navPropOpId}.GetCount-{Path.GetPathHash(Context.Settings)}";
}
else if (SecondLastSegment is ODataTypeCastSegment odataTypeCastSegment)
{
IEdmNamedElement targetStructuredType = odataTypeCastSegment.StructuredType as IEdmNamedElement;
- operation.OperationId = $"{EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, false)}.GetCount.As{Utils.UpperFirstChar(targetStructuredType.Name)}-{Path.GetPathHash(Context.Settings)}";
+ operation.OperationId = $"{EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, Context, false)}.GetCount.As{Utils.UpperFirstChar(targetStructuredType.Name)}-{Path.GetPathHash(Context.Settings)}";
}
else if (SecondLastSegment is ODataComplexPropertySegment)
{
- operation.OperationId = $"{EdmModelHelper.GenerateComplexPropertyPathOperationId(Path)}.GetCount-{Path.GetPathHash(Context.Settings)}";
+ operation.OperationId = $"{EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context)}.GetCount-{Path.GetPathHash(Context.Settings)}";
}
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs
index 8bdbacc7..743a10e8 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs
@@ -107,7 +107,7 @@ protected override void SetExtensions(OpenApiOperation operation)
internal string GetOperationId(string prefix = null)
{
- return EdmModelHelper.GenerateNavigationPropertyPathOperationId(Path, prefix);
+ return EdmModelHelper.GenerateNavigationPropertyPathOperationId(Path, Context, prefix);
}
///
diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/ODataTypeCastGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/ODataTypeCastGetOperationHandler.cs
index 84574f72..8c3231e5 100644
--- a/src/Microsoft.OpenApi.OData.Reader/Operation/ODataTypeCastGetOperationHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/Operation/ODataTypeCastGetOperationHandler.cs
@@ -188,7 +188,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
- operation.OperationId = EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path) + $".As{Utils.UpperFirstChar(TargetSchemaElement.Name)}";
+ operation.OperationId = EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, Context) + $".As{Utils.UpperFirstChar(TargetSchemaElement.Name)}";
base.SetBasicInfo(operation);
}
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs
index 69463815..8e5d9d6a 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyGetOperationHandlerTests.cs
@@ -131,6 +131,70 @@ public void CreateNavigationGetOperationViaComposableFunctionReturnsCorrectOpera
Assert.Contains(operation.Parameters, x => x.Name == "path");
}
+ [Fact]
+ public void CreateNavigationGetOperationViaOverloadedComposableFunctionReturnsCorrectOperation()
+ {
+ // Arrange
+ IEdmModel model = EdmModelHelper.GraphBetaModel;
+ ODataContext context = new(model, new OpenApiConvertSettings()
+ {
+ EnableOperationId = true
+ });
+
+ IEdmEntitySet drives = model.EntityContainer.FindEntitySet("drives");
+ IEdmEntityType drive = model.SchemaElements.OfType().First(c => c.Name == "drive");
+ IEdmNavigationProperty items = drive.DeclaredNavigationProperties().First(c => c.Name == "items");
+ IEdmEntityType driveItem = model.SchemaElements.OfType().First(c => c.Name == "driveItem");
+ IEdmNavigationProperty workbook = driveItem.DeclaredNavigationProperties().First(c => c.Name == "workbook");
+ IEdmEntityType workbookEntity = model.SchemaElements.OfType().First(c => c.Name == "workbook");
+ IEdmNavigationProperty worksheets = workbookEntity.DeclaredNavigationProperties().First(c => c.Name == "worksheets");
+ IEdmEntityType workbookWorksheet = model.SchemaElements.OfType().First(c => c.Name == "workbookWorksheet");
+ IEdmOperation usedRangeWithParams = model.SchemaElements.OfType().First(f => f.Name == "usedRange" && f.Parameters.Any(x => x.Name.Equals("valuesOnly")));
+ IEdmOperation usedRange = model.SchemaElements.OfType().First(f => f.Name == "usedRange" && f.Parameters.Count() == 1);
+ IEdmEntityType workbookRange = model.SchemaElements.OfType().First(c => c.Name == "workbookRange");
+ IEdmNavigationProperty format = workbookRange.DeclaredNavigationProperties().First(c => c.Name == "format");
+
+
+ ODataPath path1 = new(new ODataNavigationSourceSegment(drives),
+ new ODataKeySegment(drive),
+ new ODataNavigationPropertySegment(items),
+ new ODataKeySegment(driveItem),
+ new ODataNavigationPropertySegment(workbook),
+ new ODataNavigationPropertySegment(worksheets),
+ new ODataKeySegment(workbookWorksheet),
+ new ODataOperationSegment(usedRangeWithParams),
+ new ODataNavigationPropertySegment(format));
+
+ ODataPath path2 = new(new ODataNavigationSourceSegment(drives),
+ new ODataKeySegment(drive),
+ new ODataNavigationPropertySegment(items),
+ new ODataKeySegment(driveItem),
+ new ODataNavigationPropertySegment(workbook),
+ new ODataNavigationPropertySegment(worksheets),
+ new ODataKeySegment(workbookWorksheet),
+ new ODataOperationSegment(usedRange),
+ new ODataNavigationPropertySegment(format));
+
+ // Act
+ var operation1 = _operationHandler.CreateOperation(context, path1);
+ var operation2 = _operationHandler.CreateOperation(context, path2);
+
+ // Assert
+ Assert.NotNull(operation1);
+ Assert.NotNull(operation2);
+
+ Assert.Equal("drives.items.workbook.worksheets.usedRange.GetFormat-206d", operation1.OperationId);
+ Assert.Equal("drives.items.workbook.worksheets.usedRange.GetFormat-ec2c", operation2.OperationId);
+
+ Assert.NotNull(operation1.Parameters);
+ Assert.Equal(6, operation1.Parameters.Count);
+ Assert.Contains(operation1.Parameters, x => x.Name == "valuesOnly");
+
+ Assert.NotNull(operation2.Parameters);
+ Assert.Equal(5, operation2.Parameters.Count);
+ Assert.DoesNotContain(operation2.Parameters, x => x.Name == "valuesOnly");
+ }
+
[Theory]
[InlineData(true)]
[InlineData(false)]