From 272c4679b6fb4461f078d2f0b138b865e5f39cab Mon Sep 17 00:00:00 2001 From: Hardik K Date: Fri, 23 Oct 2026 19:01:22 +0530 Subject: [PATCH] fix(models): preserve tag collection types and comparers in copy constructors (#3141) fix(models): preserve tag collection types and comparers in OpenApiDocument copy constructor --- .../Models/OpenApiDocument.cs | 29 +++++----- .../Models/OpenApiDocumentTests.cs | 56 +++++++++++++++++++ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 43e5577fe..85b5ef288 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -82,21 +82,22 @@ public ISet? Tags } set { - if (value is null) - { - _tags = null; - return; - } - _tags = value switch - { - HashSet tags when 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), + }; } /// @@ -141,7 +142,7 @@ public OpenApiDocument(OpenApiDocument? document) Webhooks = document?.Webhooks != null ? new Dictionary(document.Webhooks) : null; Components = document?.Components != null ? new(document?.Components) : null; Security = document?.Security != null ? [.. document.Security] : null; - Tags = document?.Tags != null ? new HashSet(document.Tags, OpenApiTagComparer.Instance) : null; + Tags = CopyTags(document?.Tags, copy: true); ExternalDocs = document?.ExternalDocs != null ? new(document.ExternalDocs) : null; Extensions = document?.Extensions != null ? new Dictionary(document.Extensions) : null; Metadata = document?.Metadata != null ? new Dictionary(document.Metadata) : null; diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 1ef819eeb..79e5b9232 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -2214,6 +2214,62 @@ public void TagsSupportsImmutableSortedSets() Assert.IsType>(document.Tags); } + [Fact] + public void OpenApiDocumentCopyConstructorPreservesCustomComparer() + { + var document = new OpenApiDocument + { + Tags = new HashSet(new CaseInsensitiveOpenApiTagEqualityComparer()) + { + new() { Name = "Tag1" } + } + }; + + var copy = new OpenApiDocument(document); + Assert.NotNull(copy.Tags); + Assert.False(copy.Tags.Add(new OpenApiTag { Name = "tag1" })); + Assert.Single(copy.Tags); + } + + [Fact] + public void OpenApiDocumentCopyConstructorPreservesSortedSet() + { + var document = new OpenApiDocument + { + Tags = new SortedSet(new DescendingOpenApiTagComparer()) + { + new() { Name = "tagB" }, + new() { Name = "tagA" }, + new() { Name = "tagC" }, + } + }; + + var copy = new OpenApiDocument(document); + Assert.IsType>(copy.Tags); + var copyNames = copy.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], copyNames); + } + + [Fact] + public void OpenApiDocumentCopyConstructorPreservesImmutableSortedSet() + { + var document = new OpenApiDocument + { + Tags = ImmutableSortedSet.Create( + new DescendingOpenApiTagComparer(), + [ + new OpenApiTag { Name = "tagB" }, + new OpenApiTag { Name = "tagA" }, + new OpenApiTag { Name = "tagC" }, + ]), + }; + + var copy = new OpenApiDocument(document); + Assert.IsType>(copy.Tags); + var copyNames = copy.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], copyNames); + } + public static TheoryData OpenApiSpecVersions() { var values = new TheoryData();