From 45d650bfbed89d8c4adeb689a18fc11bd1ed3f32 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:48:09 +0530 Subject: [PATCH 1/8] fix(models): preserve extensions in OpenApiPaths copy constructor Signed-off-by: Hardik K --- src/Microsoft.OpenApi/Models/OpenApiPaths.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs index 1aa163e3c..9019fcaf5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; + namespace Microsoft.OpenApi { /// @@ -20,6 +22,6 @@ public OpenApiPaths() { } /// /// This creates a shallow copy, the path items are the same reference as in the provided parameter. /// - public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths) { } + public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths, extensions: paths?.Extensions != null ? new Dictionary(paths.Extensions) : null) { } } } From f79185023920f2f82d65be78a8a203bde30e1432 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:48:45 +0530 Subject: [PATCH 2/8] test(models): add unit tests for OpenApiPaths copy constructor extensions Signed-off-by: Hardik K --- .../Models/OpenApiPathsTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs new file mode 100644 index 000000000..a56def7f6 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models +{ + [Collection("DefaultSettings")] + public class OpenApiPathsTests + { + [Fact] + public void CopyConstructorCopiesExtensions() + { + // Arrange + var paths = new OpenApiPaths + { + ["/users"] = new OpenApiPathItem(), + Extensions = new Dictionary + { + ["x-custom-path"] = new JsonNodeExtension("custom-value"), + ["x-rate-limit"] = new JsonNodeExtension(100), + } + }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Equal(2, copy.Extensions.Count); + Assert.True(copy.Extensions.ContainsKey("x-custom-path")); + Assert.True(copy.Extensions.ContainsKey("x-rate-limit")); + } + + [Fact] + public void CopyConstructorIsolatesExtensionsDictionary() + { + // Arrange + var paths = new OpenApiPaths + { + Extensions = new Dictionary + { + ["x-original"] = new JsonNodeExtension("original") + } + }; + + // Act + var copy = new OpenApiPaths(paths); + copy.Extensions["x-copy-only"] = new JsonNodeExtension("copy"); + + // Assert + Assert.Equal(2, copy.Extensions.Count); + Assert.Single(paths.Extensions); + Assert.False(paths.Extensions.ContainsKey("x-copy-only")); + } + } +} From e8ba3468de92efbdaf6bf2e28c3ffd9270c87705 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:49:35 +0530 Subject: [PATCH 3/8] test(models): verify OpenApiDocument copy constructor preserves Paths extensions Signed-off-by: Hardik K --- .../Models/OpenApiDocumentTests.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 3abfb42fd..774d9ff2a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -2753,5 +2753,37 @@ public void GetOperationById_ThrowsOnNullOrEmptyId() Assert.Throws(() => doc.GetOperationById(null!)); Assert.Throws(() => doc.GetOperationById(string.Empty)); } + + [Fact] + public void OpenApiDocumentCopyConstructorPreservesPathsExtensions() + { + // Arrange + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + Paths = new OpenApiPaths + { + ["/items"] = new OpenApiPathItem(), + Extensions = new Dictionary + { + ["x-paths-summary"] = new JsonNodeExtension("Paths level extension") + } + } + }; + + // Act + var copy = new OpenApiDocument(document); + + // Assert + Assert.NotNull(copy.Paths); + Assert.NotNull(copy.Paths.Extensions); + Assert.Single(copy.Paths.Extensions); + Assert.True(copy.Paths.Extensions.ContainsKey("x-paths-summary")); + + // Verify isolation + copy.Paths.Extensions["x-isolated"] = new JsonNodeExtension("isolated"); + Assert.Single(document.Paths.Extensions); + Assert.False(document.Paths.Extensions.ContainsKey("x-isolated")); + } } } From 681c99750077ab2e9573400e4a3b94e24f688224 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:50:06 +0530 Subject: [PATCH 4/8] fix(models): preserve extensions in OpenApiResponses copy constructor Signed-off-by: Hardik K --- src/Microsoft.OpenApi/Models/OpenApiResponses.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs index 63a144a2a..fef65d8c7 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs @@ -1,6 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; + namespace Microsoft.OpenApi { /// @@ -17,6 +19,6 @@ public OpenApiResponses() { } /// Initializes a copy of object /// /// The - public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses) { } + public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses, extensions: openApiResponses?.Extensions != null ? new Dictionary(openApiResponses.Extensions) : null) { } } } From 35661553709762b7d255ca4aaae9a1d5dc9666fa Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:50:46 +0530 Subject: [PATCH 5/8] test(models): add unit tests for OpenApiResponses copy constructor extensions Signed-off-by: Hardik K --- .../Models/OpenApiResponsesTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs new file mode 100644 index 000000000..ffc5b3834 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Models +{ + [Collection("DefaultSettings")] + public class OpenApiResponsesTests + { + [Fact] + public void CopyConstructorCopiesExtensions() + { + // Arrange + var responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "OK" }, + Extensions = new Dictionary + { + ["x-response-notes"] = new JsonNodeExtension("all-responses-note"), + ["x-default-code"] = new JsonNodeExtension("200"), + } + }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Equal(2, copy.Extensions.Count); + Assert.True(copy.Extensions.ContainsKey("x-response-notes")); + Assert.True(copy.Extensions.ContainsKey("x-default-code")); + } + + [Fact] + public void CopyConstructorIsolatesExtensionsDictionary() + { + // Arrange + var responses = new OpenApiResponses + { + Extensions = new Dictionary + { + ["x-original"] = new JsonNodeExtension("original") + } + }; + + // Act + var copy = new OpenApiResponses(responses); + copy.Extensions["x-copy-only"] = new JsonNodeExtension("copy"); + + // Assert + Assert.Equal(2, copy.Extensions.Count); + Assert.Single(responses.Extensions); + Assert.False(responses.Extensions.ContainsKey("x-copy-only")); + } + } +} From 0344cf6a811189433d077f5170eb98e1b2c5b711 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:51:35 +0530 Subject: [PATCH 6/8] test(models): verify OpenApiOperation copy constructor preserves Responses extensions Signed-off-by: Hardik K --- .../Models/OpenApiOperationTests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index b43ef7b53..8a9af17cc 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -995,5 +995,37 @@ public void OpenApiOperationCopyConstructorPreservesImmutableSortedSet() var copyNames = copy.Tags.Select(t => t.Name); Assert.Equal(["tagC", "tagB", "tagA"], copyNames); } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesResponsesExtensions() + { + // Arrange + var operation = new OpenApiOperation + { + OperationId = "getItem", + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse { Description = "Success" }, + Extensions = new Dictionary + { + ["x-responses-meta"] = new JsonNodeExtension("Metadata for responses") + } + } + }; + + // Act + var copy = new OpenApiOperation(operation); + + // Assert + Assert.NotNull(copy.Responses); + Assert.NotNull(copy.Responses.Extensions); + Assert.Single(copy.Responses.Extensions); + Assert.True(copy.Responses.Extensions.ContainsKey("x-responses-meta")); + + // Verify isolation + copy.Responses.Extensions["x-isolated"] = new JsonNodeExtension("isolated"); + Assert.Single(operation.Responses.Extensions); + Assert.False(operation.Responses.Extensions.ContainsKey("x-isolated")); + } } } From 93206f58fdc085c2bc555f84b4333b0109dce7ca Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:52:27 +0530 Subject: [PATCH 7/8] test(models): add edge case tests for null and empty extensions in Paths and Responses copy constructors Signed-off-by: Hardik K --- .../Models/OpenApiPathsTests.cs | 43 +++++++++++++++++++ .../Models/OpenApiResponsesTests.cs | 43 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs index a56def7f6..c4ebc20df 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiPathsTests.cs @@ -54,5 +54,48 @@ public void CopyConstructorIsolatesExtensionsDictionary() Assert.Single(paths.Extensions); Assert.False(paths.Extensions.ContainsKey("x-copy-only")); } + + [Fact] + public void CopyConstructorWithNullPathsInitializesEmptyExtensions() + { + // Act + var copy = new OpenApiPaths(null!); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + Assert.Empty(copy); + } + + [Fact] + public void CopyConstructorWithNullExtensionsInitializesEmptyExtensions() + { + // Arrange + var paths = new OpenApiPaths { Extensions = null }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + + [Fact] + public void CopyConstructorWithEmptyExtensionsInitializesEmptyExtensions() + { + // Arrange + var paths = new OpenApiPaths + { + Extensions = new Dictionary() + }; + + // Act + var copy = new OpenApiPaths(paths); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs index ffc5b3834..35390ccb4 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponsesTests.cs @@ -54,5 +54,48 @@ public void CopyConstructorIsolatesExtensionsDictionary() Assert.Single(responses.Extensions); Assert.False(responses.Extensions.ContainsKey("x-copy-only")); } + + [Fact] + public void CopyConstructorWithNullResponsesInitializesEmptyExtensions() + { + // Act + var copy = new OpenApiResponses(null!); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + Assert.Empty(copy); + } + + [Fact] + public void CopyConstructorWithNullExtensionsInitializesEmptyExtensions() + { + // Arrange + var responses = new OpenApiResponses { Extensions = null }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } + + [Fact] + public void CopyConstructorWithEmptyExtensionsInitializesEmptyExtensions() + { + // Arrange + var responses = new OpenApiResponses + { + Extensions = new Dictionary() + }; + + // Act + var copy = new OpenApiResponses(responses); + + // Assert + Assert.NotNull(copy.Extensions); + Assert.Empty(copy.Extensions); + } } } From fe37546acbad8a81ac354bf3371ed65e26d07784 Mon Sep 17 00:00:00 2001 From: Hardik K Date: Thu, 5 Nov 2026 23:53:01 +0530 Subject: [PATCH 8/8] docs(models): update XML documentation on OpenApiPaths and OpenApiResponses copy constructors Signed-off-by: Hardik K --- src/Microsoft.OpenApi/Models/OpenApiPaths.cs | 5 +++-- src/Microsoft.OpenApi/Models/OpenApiResponses.cs | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs index 9019fcaf5..d79c95320 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs @@ -16,11 +16,12 @@ public class OpenApiPaths : OpenApiExtensibleDictionary public OpenApiPaths() { } /// - /// Initializes a copy of object + /// Initializes a copy of object. /// /// The . /// - /// This creates a shallow copy, the path items are the same reference as in the provided parameter. + /// This creates a shallow copy; the path items are the same reference as in the provided parameter, + /// and specification extensions are copied into a new dictionary. /// public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths, extensions: paths?.Extensions != null ? new Dictionary(paths.Extensions) : null) { } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs index fef65d8c7..f510c9cd5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs @@ -16,9 +16,13 @@ public class OpenApiResponses : OpenApiExtensibleDictionary public OpenApiResponses() { } /// - /// Initializes a copy of object + /// Initializes a copy of object. /// - /// The + /// The . + /// + /// This creates a shallow copy; the responses are the same reference as in the provided parameter, + /// and specification extensions are copied into a new dictionary. + /// public OpenApiResponses(OpenApiResponses openApiResponses) : base(dictionary: openApiResponses, extensions: openApiResponses?.Extensions != null ? new Dictionary(openApiResponses.Extensions) : null) { } } }