From c4ee9ff5157a13237b031cafacd29e077f9fcf2e Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 9 Jan 2026 11:43:46 -0800 Subject: [PATCH 1/4] feat: add switch to use put as default update verb --- .../OpenApiConvertSettings.cs | 12 ++++++- .../PathItem/ComplexPropertyItemHandler.cs | 11 ++++++- .../PathItem/EntityPathItemHandler.cs | 11 ++++++- .../NavigationPropertyPathItemHandler.cs | 11 ++++++- .../PublicAPI.Unshipped.txt | 2 ++ src/OoasUtil/ComLineProcessor.cs | 32 +++++++++++++++++++ src/OoasUtil/Program.cs | 1 + 7 files changed, 76 insertions(+), 4 deletions(-) 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) From 326f9511d2744d0abaf4147070524b9c250dbf46 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 9 Jan 2026 12:41:19 -0800 Subject: [PATCH 2/4] fix version time-bomb --- tool/versioning.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/versioning.props b/tool/versioning.props index b6bd275b..5caaa033 100644 --- a/tool/versioning.props +++ b/tool/versioning.props @@ -29,7 +29,7 @@ overflows the Int16. The system convert below will throw errors when this happens. --> - 2020 + 2026 $([System.Convert]::ToInt32('$([MSBuild]::Add(1, $([MSBuild]::Subtract($([System.DateTime]::Now.Year), $(VersionStartYear)))))$([System.DateTime]::Now.ToString("MMdd"))')) From d9195fbeb44b8cb56ebaa608a923009a322f1bf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:52:06 +0000 Subject: [PATCH 3/4] Initial plan From 9e1b6bd1f53b0cf8ebd15335e261f6edc34cc1b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:59:30 +0000 Subject: [PATCH 4/4] Add unit tests for UseHttpPutForUpdate feature Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../ComplexPropertyPathItemHandlerTests.cs | 97 ++++++++++++++ .../PathItem/EntityPathItemHandlerTests.cs | 61 +++++++++ .../NavigationPropertyPathItemHandlerTests.cs | 122 ++++++++++++++++++ 3 files changed, 280 insertions(+) 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 = @"