From 362b2729a26c6c72025dd96d0f019bb79c788c0e Mon Sep 17 00:00:00 2001 From: Abhikr Date: Tue, 13 Oct 2026 07:09:59 +0530 Subject: [PATCH 1/2] fix: support reinitializing OpenApiOperation.Tags to null and custom comparers Align OpenApiOperation.Tags setter and copy constructor with OpenApiDocument.Tags to avoid silently ignoring null assignments and to preserve custom equality comparers or sorted sets. --- .../Models/OpenApiOperation.cs | 26 ++++- .../Models/OpenApiOperationTests.cs | 94 +++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index ee1afcc0a..c9878d132 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +#if NET +using System.Collections.Immutable; +#endif using System.Linq; namespace Microsoft.OpenApi @@ -27,11 +30,18 @@ public ISet? Tags { if (value is null) { + _tags = null; return; } - _tags = value is HashSet tags && tags.Comparer is OpenApiTagComparer ? - tags : - new HashSet(value, OpenApiTagComparer.Instance); + _tags = value switch + { + HashSet tags when tags.Comparer != EqualityComparer.Default => value, + SortedSet => value, +#if NET + ImmutableSortedSet => value, +#endif + _ => new HashSet(value, OpenApiTagComparer.Instance), + }; } } @@ -129,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 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 d281f4437..c9c53ca14 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -851,5 +851,99 @@ public void OpenApiOperationCopyConstructorWithAnnotationsSucceeds() Assert.NotEqual(baseOperation.Metadata["key1"], actualOperation.Metadata["key1"]); } + + [Fact] + public void TagsCanBeReInitializedToNull() + { + var operation = new OpenApiOperation(); + Assert.Null(operation.Tags); + operation.Tags = new HashSet(); + Assert.NotNull(operation.Tags); + operation.Tags = null; + Assert.Null(operation.Tags); + } + + [Fact] + public void TagsSupportsCustomComparer() + { + var operation = new OpenApiOperation + { + Tags = new HashSet(new CaseInsensitiveOpenApiTagReferenceEqualityComparer()), + }; + + Assert.True(operation.Tags.Add(new OpenApiTagReference("Tag1"))); + Assert.False(operation.Tags.Add(new OpenApiTagReference("tag1"))); + Assert.True(operation.Tags.Add(new OpenApiTagReference("tag2"))); + Assert.False(operation.Tags.Add(new OpenApiTagReference("TAG1"))); + Assert.Equal(2, operation.Tags.Count); + } + + [Fact] + public void TagsSupportsSortedSet() + { + var operation = new OpenApiOperation + { + Tags = new SortedSet(new TagReferenceComparer()), + }; + + Assert.IsType>(operation.Tags); + } + + [Fact] + public void OpenApiOperationCopyConstructorPreservesCustomComparer() + { + var operation = new OpenApiOperation + { + Tags = new HashSet(new CaseInsensitiveOpenApiTagReferenceEqualityComparer()) + { + new OpenApiTagReference("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 TagReferenceComparer()) + { + new OpenApiTagReference("b"), + new OpenApiTagReference("a") + } + }; + + var copy = new OpenApiOperation(operation); + Assert.IsType>(copy.Tags); + Assert.Collection(copy.Tags, + tag => Assert.Equal("a", tag.Name), + tag => Assert.Equal("b", tag.Name)); + } + + private sealed class TagReferenceComparer : IComparer + { + public int Compare(OpenApiTagReference x, OpenApiTagReference y) + { + return string.Compare(x?.Name, y?.Name, System.StringComparison.Ordinal); + } + } + + private sealed class CaseInsensitiveOpenApiTagReferenceEqualityComparer : IEqualityComparer + { + public bool Equals(OpenApiTagReference x, OpenApiTagReference y) + { + return string.Equals(x?.Name, y?.Name, System.StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(OpenApiTagReference obj) + { + return obj?.Name == null ? 0 : System.StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name); + } + } } } From e2dc388e301dc12617936258f425a2f6204563b4 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 13 Oct 2026 18:59:05 +0530 Subject: [PATCH 2/2] Update OpenApiOperation test to use custom comparer Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Models/OpenApiOperationTests.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index c9c53ca14..b07526b6d 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -909,20 +909,26 @@ public void OpenApiOperationCopyConstructorPreservesCustomComparer() [Fact] public void OpenApiOperationCopyConstructorPreservesSortedSet() { + var comparer = Comparer.Create((x, y) => + string.Compare(y?.Name, x?.Name, System.StringComparison.Ordinal)); var operation = new OpenApiOperation { - Tags = new SortedSet(new TagReferenceComparer()) + Tags = new SortedSet(comparer) { new OpenApiTagReference("b"), new OpenApiTagReference("a") } }; + Assert.Collection(operation.Tags, + tag => Assert.Equal("b", tag.Name), + tag => Assert.Equal("a", tag.Name)); + var copy = new OpenApiOperation(operation); Assert.IsType>(copy.Tags); Assert.Collection(copy.Tags, - tag => Assert.Equal("a", tag.Name), - tag => Assert.Equal("b", tag.Name)); + tag => Assert.Equal("b", tag.Name), + tag => Assert.Equal("a", tag.Name)); } private sealed class TagReferenceComparer : IComparer