From 376b09957e6ec01f28340535f850ee74e00d9501 Mon Sep 17 00:00:00 2001 From: Hardik Kaurani Date: Tue, 20 Oct 2026 00:30:05 +0530 Subject: [PATCH] fix: Support custom tag ordering and null assignment in OpenApiOperation.Tags Fixes #3115 --- .../Models/OpenApiOperation.cs | 18 +++- .../Models/OpenApiOperationTests.cs | 90 ++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index ee1afcc0a..f915bed01 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -1,8 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. 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), + }; } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index d281f4437..46e25c183 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -1,7 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text.Json.Nodes; using System.Threading.Tasks; using Xunit; @@ -851,5 +855,89 @@ 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); + } + + private sealed class CaseInsensitiveOpenApiTagReferenceEqualityComparer : IEqualityComparer + { + public bool Equals(OpenApiTagReference x, OpenApiTagReference y) + { + return string.Equals(x?.Name, y?.Name, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode([DisallowNull] OpenApiTagReference obj) + { + return obj.Name?.GetHashCode(StringComparison.OrdinalIgnoreCase) ?? 0; + } + } + + [Fact] + public void TagsSupportsSortedSets() + { + var operation = new OpenApiOperation + { + Tags = new SortedSet(new DescendingOpenApiTagReferenceComparer()) + { + new("tagB"), + new("tagA"), + new("tagC"), + } + }; + + var names = operation.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], names); + Assert.IsType>(operation.Tags); + } + + private sealed class DescendingOpenApiTagReferenceComparer : IComparer + { + public int Compare(OpenApiTagReference x, OpenApiTagReference y) + { + return string.Compare(y?.Name, x?.Name, StringComparison.Ordinal); + } + } + + [Fact] + public void TagsSupportsImmutableSortedSets() + { + var operation = new OpenApiOperation + { + Tags = ImmutableSortedSet.Create( + new DescendingOpenApiTagReferenceComparer(), + [ + new OpenApiTagReference("tagB"), + new OpenApiTagReference("tagA"), + new OpenApiTagReference("tagC"), + ]), + }; + + var names = operation.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], names); + Assert.IsType>(operation.Tags); + } } }