From f417cee1e269c0d3508640f1d5f1dc5c763ed987 Mon Sep 17 00:00:00 2001 From: Austin Aviv Date: Wed, 21 Oct 2026 23:12:51 +0530 Subject: [PATCH] fix: support reinitializing OpenApiOperation.Tags to null and custom comparers (#3116) * 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. * 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 | 29 +++++----- .../Models/OpenApiOperationTests.cs | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 43d283d7b..160cba403 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,7 +140,7 @@ public OpenApiOperation() { } public OpenApiOperation(OpenApiOperation operation) { Utils.CheckArgumentNull(operation); - Tags = operation.Tags != null ? new HashSet(operation.Tags) : null; + Tags = CopyTags(operation.Tags, copy: true); 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 0639175f0..e4b70785a 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); + } } }