diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
index a7021c44..4ea0a38e 100644
--- a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
@@ -337,6 +337,15 @@ public string? PathPrefix
///
public int ComposableFunctionsExpansionDepth { get; set; } = 1;
+ ///
+ /// Gets/sets a value indicating whether to use HTTP PUT method for update operations by default
+ /// instead of PATCH when no UpdateRestrictions annotation is present in the CSDL.
+ /// If false (default), PATCH will be used for updates.
+ /// If true, PUT will be used for updates.
+ /// This setting is ignored when UpdateRestrictions annotations are present in the CSDL.
+ ///
+ public bool UseHttpPutForUpdate { get; set; } = false;
+
internal OpenApiConvertSettings Clone()
{
var newSettings = new OpenApiConvertSettings
@@ -392,7 +401,8 @@ internal OpenApiConvertSettings Clone()
SemVerVersion = this.SemVerVersion,
EnableAliasForOperationSegments = this.EnableAliasForOperationSegments,
UseStringArrayForQueryOptionsSchema = this.UseStringArrayForQueryOptionsSchema,
- ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth
+ ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth,
+ UseHttpPutForUpdate = this.UseHttpPutForUpdate
};
return newSettings;
diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs
index 6596750e..1e4e7a83 100644
--- a/src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs
@@ -99,7 +99,16 @@ public void AddUpdateOperation(OpenApiPathItem item)
}
else
{
- AddOperation(item, HttpMethod.Patch);
+ // When no explicit update method is specified in UpdateRestrictions,
+ // use the UseHttpPutForUpdate setting to determine the default method
+ if (Context?.Settings?.UseHttpPutForUpdate == true)
+ {
+ AddOperation(item, HttpMethod.Put);
+ }
+ else
+ {
+ AddOperation(item, HttpMethod.Patch);
+ }
}
}
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs
index 0487f787..0d4dbdbd 100644
--- a/src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs
@@ -67,7 +67,16 @@ protected override void SetOperations(OpenApiPathItem item)
}
else
{
- AddOperation(item, HttpMethod.Patch);
+ // When no explicit update method is specified in UpdateRestrictions,
+ // use the UseHttpPutForUpdate setting to determine the default method
+ if (Context?.Settings?.UseHttpPutForUpdate == true)
+ {
+ AddOperation(item, HttpMethod.Put);
+ }
+ else
+ {
+ AddOperation(item, HttpMethod.Patch);
+ }
}
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs
index a1c33b15..a86fc5da 100644
--- a/src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs
+++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs
@@ -279,7 +279,16 @@ private void AddUpdateOperation(OpenApiPathItem item, UpdateRestrictionsType? up
}
else
{
- AddOperation(item, HttpMethod.Patch);
+ // When no explicit update method is specified in UpdateRestrictions,
+ // use the UseHttpPutForUpdate setting to determine the default method
+ if (Context?.Settings?.UseHttpPutForUpdate == true)
+ {
+ AddOperation(item, HttpMethod.Put);
+ }
+ else
+ {
+ AddOperation(item, HttpMethod.Patch);
+ }
}
}
diff --git a/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
index e29f3677..ee063ec9 100644
--- a/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
@@ -221,6 +221,8 @@ Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSche
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSchema.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.set -> void
+Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.get -> bool
+Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.set -> void
Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey
diff --git a/src/OoasUtil/ComLineProcessor.cs b/src/OoasUtil/ComLineProcessor.cs
index 84cb58c3..b8169d50 100644
--- a/src/OoasUtil/ComLineProcessor.cs
+++ b/src/OoasUtil/ComLineProcessor.cs
@@ -99,6 +99,11 @@ public ComLineProcessor(string[] args)
///
public bool? RequireDerivedTypesConstraint { get; private set; }
+ ///
+ /// Use HTTP PUT method for update operations by default instead of PATCH.
+ ///
+ public bool? UseHttpPutForUpdate { get; private set; }
+
///
/// Process the arguments.
///
@@ -227,6 +232,14 @@ public bool Process()
}
break;
+ case "--useputforupdate":
+ case "-put":
+ if (!ProcessUseHttpPutForUpdate(true))
+ {
+ return false;
+ }
+ break;
+
default:
PrintUsage();
return false;
@@ -285,6 +298,11 @@ public bool Process()
DisableSchemaExamples = false;
}
+ if (UseHttpPutForUpdate == null)
+ {
+ UseHttpPutForUpdate = false;
+ }
+
_continue = ValidateArguments();
return _continue;
}
@@ -419,6 +437,19 @@ private bool ProcessDisableSchemaExamples(bool disableSchemaExamples)
return true;
}
+ private bool ProcessUseHttpPutForUpdate(bool useHttpPutForUpdate)
+ {
+ if (UseHttpPutForUpdate != null)
+ {
+ Console.WriteLine("[Error:] Multiple [--useputforupdate|-put] are not allowed.\n");
+ PrintUsage();
+ return false;
+ }
+
+ UseHttpPutForUpdate = useHttpPutForUpdate;
+ return true;
+ }
+
private bool ProcessTarget(int version)
{
if (Version != null)
@@ -484,6 +515,7 @@ public static void PrintUsage()
sb.Append(" --enablepagination|-p\t\t\tSet the output to expose pagination for collections.\n");
sb.Append(" --enableunqualifiedcall|-u\t\t\tSet the output to use unqualified calls for bound operations.\n");
sb.Append(" --disableschemaexamples|-x\t\t\tDisable examples in the schema.\n");
+ sb.Append(" --useputforupdate|-put\t\t\tUse HTTP PUT method for update operations instead of PATCH by default.\n");
sb.Append(" --json|-j\t\t\tSet the output format as JSON.\n");
sb.Append(" --yaml|-y\t\t\tSet the output format as YAML.\n");
sb.Append(" --specversion|-s IntVersion\tSet the OpenApi Specification version of the output. Only 2 or 3 are supported.\n");
diff --git a/src/OoasUtil/Program.cs b/src/OoasUtil/Program.cs
index 211dc0f5..937abdde 100644
--- a/src/OoasUtil/Program.cs
+++ b/src/OoasUtil/Program.cs
@@ -42,6 +42,7 @@ static async System.Threading.Tasks.Task Main(string[] args)
EnableUnqualifiedCall = processor.EnableUnqualifiedCall.Value,
ShowSchemaExamples = !processor.DisableSchemaExamples.Value,
OpenApiSpecVersion = processor.Version.Value,
+ UseHttpPutForUpdate = processor.UseHttpPutForUpdate.Value,
};
if (processor.IsLocalFile)
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/ComplexPropertyPathItemHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/ComplexPropertyPathItemHandlerTests.cs
index 0a413cd1..28345303 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/ComplexPropertyPathItemHandlerTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/ComplexPropertyPathItemHandlerTests.cs
@@ -263,4 +263,101 @@ public void CreatesComplexPropertyPathsBasedOnTargetPathAnnotations(string reada
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
}
}
+
+ [Theory]
+ [InlineData(false, 2)]
+ [InlineData(true, 2)]
+ public void CreatesComplexPropertyPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, int operationCount)
+ {
+ // Arrange
+ var annotation = @"
+
+
+
+
+
+
+
+
+
+";
+ var target = @"""NS.Customer/BillingAddress""";
+ var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
+ var convertSettings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = useHttpPutForUpdate
+ };
+ var context = new ODataContext(model, convertSettings);
+ var entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ var entityType = entitySet.EntityType;
+ var property = entityType.FindProperty("BillingAddress");
+ Assert.NotNull(property); // guard
+ var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
+ Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.Equal(operationCount, pathItem.Operations?.Count ?? 0);
+
+ Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
+ if (useHttpPutForUpdate)
+ {
+ Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
+ Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
+ }
+ else
+ {
+ Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Patch));
+ Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Put));
+ }
+ }
+
+ [Fact]
+ public void CreateComplexPropertyPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
+ {
+ // Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
+ var annotation = @"
+
+
+
+ Org.OData.Capabilities.V1.HttpMethod/PUT
+
+
+
+
+
+
+
+
+";
+ var target = @"""NS.Customer/BillingAddress""";
+ var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
+ var convertSettings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = false // Setting says use PATCH (default)
+ };
+ var context = new ODataContext(model, convertSettings);
+ var entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ var entityType = entitySet.EntityType;
+ var property = entityType.FindProperty("BillingAddress");
+ Assert.NotNull(property); // guard
+ var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
+ Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.Equal(2, pathItem.Operations?.Count ?? 0);
+ Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
+ // Should use PUT from annotation, not PATCH from setting
+ Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
+ Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
+ }
}
\ No newline at end of file
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs
index 7d6c6c61..92885c16 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs
@@ -232,6 +232,67 @@ public void CreateEntityPathItemWorksForUpdateMethodRestrictionsCapabilities(boo
VerifyPathItemOperations(annotation, expected);
}
+ [Theory]
+ [InlineData(false, new string[] { "get", "patch", "delete" })]
+ [InlineData(true, new string[] { "get", "put", "delete" })]
+ public void CreateEntityPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, string[] expected)
+ {
+ // Arrange
+ IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: "");
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = useHttpPutForUpdate
+ };
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(expected, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));
+ }
+
+ [Fact]
+ public void CreateEntityPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
+ {
+ // Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
+ string annotation = @"
+
+
+
+ Org.OData.Capabilities.V1.HttpMethod/PUT
+
+
+";
+
+ IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation);
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = false // Setting says use PATCH (default)
+ };
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ // Should use PUT from annotation, not PATCH from setting
+ Assert.Equal(new string[] { "get", "put", "delete" }, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));
+ }
+
private void VerifyPathItemOperations(string annotation, string[] expected)
{
// Arrange
diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/NavigationPropertyPathItemHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/NavigationPropertyPathItemHandlerTests.cs
index 32c456d7..76b43541 100644
--- a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/NavigationPropertyPathItemHandlerTests.cs
+++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/NavigationPropertyPathItemHandlerTests.cs
@@ -602,6 +602,128 @@ public void CreateNavigationPropertyPathItemAddsCustomAttributeValuesToPathExten
Assert.Equal("true", isHiddenValue);
}
+ [Theory]
+ [InlineData(false, new string[] { "get", "patch", "delete" })]
+ [InlineData(true, new string[] { "get", "put", "delete" })]
+ public void CreateSingleNavigationPropertyPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, string[] expected)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel("");
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = useHttpPutForUpdate
+ };
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType;
+
+ IEdmNavigationProperty property = entityType.DeclaredNavigationProperties()
+ .FirstOrDefault(c => c.ContainsTarget == true && c.TargetMultiplicity() != EdmMultiplicity.Many);
+ Assert.NotNull(property);
+
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataNavigationPropertySegment(property));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(expected, pathItem.Operations.Select(o => o.Key.ToString().ToLowerInvariant()));
+ }
+
+ [Theory]
+ [InlineData(false, new string[] { "get", "patch", "delete" })]
+ [InlineData(true, new string[] { "get", "put", "delete" })]
+ public void CreateCollectionNavigationPropertyPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, string[] expected)
+ {
+ // Arrange
+ IEdmModel model = GetEdmModel("");
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = useHttpPutForUpdate
+ };
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType;
+
+ IEdmNavigationProperty property = entityType.DeclaredNavigationProperties()
+ .FirstOrDefault(c => c.ContainsTarget == true && c.TargetMultiplicity() == EdmMultiplicity.Many);
+ Assert.NotNull(property);
+
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataNavigationPropertySegment(property),
+ new ODataKeySegment(property.ToEntityType()));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ Assert.Equal(expected, pathItem.Operations.Select(o => o.Key.ToString().ToLowerInvariant()));
+ }
+
+ [Fact]
+ public void CreateNavigationPropertyPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
+ {
+ // Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
+ string annotation = @"
+
+
+
+
+
+
+
+
+
+ Org.OData.Capabilities.V1.HttpMethod/PUT
+
+
+
+
+
+
+
+";
+
+ IEdmModel model = GetEdmModel(annotation);
+ OpenApiConvertSettings settings = new OpenApiConvertSettings
+ {
+ UseHttpPutForUpdate = false // Setting says use PATCH (default)
+ };
+ ODataContext context = new ODataContext(model, settings);
+ IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
+ Assert.NotNull(entitySet); // guard
+ IEdmEntityType entityType = entitySet.EntityType;
+
+ IEdmNavigationProperty property = entityType.DeclaredNavigationProperties()
+ .FirstOrDefault(c => c.ContainsTarget == true && c.Name == "ContainedMyOrder");
+ Assert.NotNull(property);
+
+ ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet),
+ new ODataKeySegment(entityType),
+ new ODataNavigationPropertySegment(property));
+
+ // Act
+ var pathItem = _pathItemHandler.CreatePathItem(context, path);
+
+ // Assert
+ Assert.NotNull(pathItem);
+ Assert.NotNull(pathItem.Operations);
+ Assert.NotEmpty(pathItem.Operations);
+ // Should use PUT from annotation, not PATCH from setting
+ Assert.Equal(new string[] { "get", "put", "delete" }, pathItem.Operations.Select(o => o.Key.ToString().ToLowerInvariant()));
+ }
+
public static IEdmModel GetEdmModel(string annotation, string annotation2 = "")
{
const string template = @"