From eb6ec97ec404576215c23115b9078615913375d2 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Thu, 22 Oct 2026 00:30:07 +0530 Subject: [PATCH] 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 03086600d..de2bb9d16 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -87,21 +87,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), + }; } /// @@ -147,7 +148,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 8b4629664..3abfb42fd 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();