From a8ee1deef21b5e54c9e0a2d2618a69a30a2cc950 Mon Sep 17 00:00:00 2001 From: Abhikr Date: Wed, 21 Oct 2026 08:48:42 +0530 Subject: [PATCH 1/2] fix: preserve custom tag comparers and sorted sets in OpenApiOperation copy constructor Align the OpenApiOperation copy constructor with OpenApiOperation.Tags setter to preserve custom equality comparers, sorted sets, and immutable sorted sets, and add corresponding copy constructor unit tests. --- .../Models/OpenApiOperation.cs | 10 +++- .../Models/OpenApiOperationTests.cs | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 20ce8bf53..d2de19612 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -139,7 +139,15 @@ public OpenApiOperation() { } public OpenApiOperation(OpenApiOperation operation) { Utils.CheckArgumentNull(operation); - Tags = operation.Tags != null ? new HashSet(operation.Tags) : null; + Tags = operation.Tags switch + { + HashSet tags when !ReferenceEquals(tags.Comparer, EqualityComparer.Default) => new HashSet(tags, tags.Comparer), + SortedSet sorted => new SortedSet(sorted, sorted.Comparer), +#if NET + ImmutableSortedSet => operation.Tags, +#endif + _ => operation.Tags != null ? new HashSet(operation.Tags, OpenApiTagComparer.Instance) : null, + }; Summary = operation.Summary ?? Summary; Description = operation.Description ?? Description; ExternalDocs = operation.ExternalDocs != null ? new(operation.ExternalDocs) : null; diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index 46e25c183..b43ef7b53 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -939,5 +939,61 @@ public void TagsSupportsImmutableSortedSets() Assert.Equal(["tagC", "tagB", "tagA"], names); Assert.IsType>(operation.Tags); } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesCustomComparer() + { + var operation = new OpenApiOperation + { + Tags = new HashSet(new CaseInsensitiveOpenApiTagReferenceEqualityComparer()) + { + new("Tag1") + } + }; + + var copy = new OpenApiOperation(operation); + Assert.NotNull(copy.Tags); + Assert.False(copy.Tags.Add(new OpenApiTagReference("tag1"))); + Assert.Single(copy.Tags); + } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesSortedSet() + { + var operation = new OpenApiOperation + { + Tags = new SortedSet(new DescendingOpenApiTagReferenceComparer()) + { + new("tagB"), + new("tagA"), + new("tagC"), + } + }; + + var copy = new OpenApiOperation(operation); + Assert.IsType>(copy.Tags); + var copyNames = copy.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], copyNames); + } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesImmutableSortedSet() + { + var operation = new OpenApiOperation + { + Tags = ImmutableSortedSet.Create( + new DescendingOpenApiTagReferenceComparer(), + [ + new OpenApiTagReference("tagB"), + new OpenApiTagReference("tagA"), + new OpenApiTagReference("tagC"), + ]), + }; + + var copy = new OpenApiOperation(operation); + Assert.IsType>(copy.Tags); + var copyNames = copy.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], copyNames); + } } } From af8c1dc72eee4333fa0fb4bb601b507d894592a3 Mon Sep 17 00:00:00 2001 From: Abhikr Date: Wed, 21 Oct 2026 23:04:02 +0530 Subject: [PATCH 2/2] refactor: deduplicate tag normalization logic into static helper Extract CopyTags static method to deduplicate tag normalization logic between OpenApiOperation.Tags setter and the copy constructor as requested in review. --- .../Models/OpenApiOperation.cs | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index d2de19612..0a79481f0 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -28,21 +28,22 @@ public ISet? Tags } set { - if (value is null) - { - _tags = null; - return; - } - _tags = value switch - { - HashSet tags when !ReferenceEquals(tags.Comparer, EqualityComparer.Default) => value, - SortedSet => value, + _tags = CopyTags(value); + } + } + + private static ISet? CopyTags(ISet? tags, bool copy = false) + { + return tags switch + { + null => null, + HashSet hashSet when !ReferenceEquals(hashSet.Comparer, EqualityComparer.Default) => copy ? new HashSet(hashSet, hashSet.Comparer) : hashSet, + SortedSet sorted => copy ? new SortedSet(sorted, sorted.Comparer) : sorted, #if NET - ImmutableSortedSet => value, + ImmutableSortedSet => tags, #endif - _ => new HashSet(value, OpenApiTagComparer.Instance), - }; - } + _ => new HashSet(tags, OpenApiTagComparer.Instance), + }; } /// @@ -139,15 +140,7 @@ public OpenApiOperation() { } public OpenApiOperation(OpenApiOperation operation) { Utils.CheckArgumentNull(operation); - Tags = operation.Tags switch - { - HashSet tags when !ReferenceEquals(tags.Comparer, EqualityComparer.Default) => new HashSet(tags, tags.Comparer), - SortedSet sorted => new SortedSet(sorted, sorted.Comparer), -#if NET - ImmutableSortedSet => operation.Tags, -#endif - _ => operation.Tags != null ? new HashSet(operation.Tags, OpenApiTagComparer.Instance) : null, - }; + Tags = CopyTags(operation.Tags, copy: true); Summary = operation.Summary ?? Summary; Description = operation.Description ?? Description; ExternalDocs = operation.ExternalDocs != null ? new(operation.ExternalDocs) : null;