From 7610d07bd11a99a4ce1cc31338d180dbd764d952 Mon Sep 17 00:00:00 2001 From: Paul Spangler <7519484+spanglerco@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:33:52 -0600 Subject: [PATCH] fix: Support custom tag ordering --- .../Models/OpenApiDocument.cs | 15 +++- .../Models/OpenApiDocumentTests.cs | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 031eadb5d..3f12efef1 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +#if NET +using System.Collections.Immutable; +#endif using System.IO; using System.Linq; using System.Security.Cryptography; @@ -83,9 +86,15 @@ public ISet? Tags { 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/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 5e3644f1b..8480269d2 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using VerifyXunit; @@ -2126,6 +2129,79 @@ public void DeduplicatesTags() Assert.Contains(document.Tags, t => t.Name == "tag2"); } + [Fact] + public void TagsSupportsCustomComparer() + { + var document = new OpenApiDocument + { + Tags = new HashSet(new CaseInsensitiveOpenApiTagEqualityComparer()), + }; + + Assert.True(document.Tags.Add(new OpenApiTag { Name = "Tag1" })); + Assert.False(document.Tags.Add(new OpenApiTag { Name = "tag1" })); + Assert.True(document.Tags.Add(new OpenApiTag { Name = "tag2" })); + Assert.False(document.Tags.Add(new OpenApiTag { Name = "TAG1" })); + Assert.Equal(2, document.Tags.Count); + } + + private sealed class CaseInsensitiveOpenApiTagEqualityComparer : IEqualityComparer + { + public bool Equals(OpenApiTag x, OpenApiTag y) + { + return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode([DisallowNull] OpenApiTag obj) + { + return obj.Name.GetHashCode(StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + public void TagsSupportsSortedSets() + { + var document = new OpenApiDocument + { + Tags = new SortedSet(new DescendingOpenApiTagComparer()) + { + new OpenApiTag { Name = "tagB" }, + new OpenApiTag { Name = "tagA" }, + new OpenApiTag { Name = "tagC" }, + } + }; + + var names = document.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], names); + Assert.IsType>(document.Tags); + } + + private sealed class DescendingOpenApiTagComparer : IComparer + { + public int Compare(OpenApiTag x, OpenApiTag y) + { + return string.Compare(y?.Name, x?.Name, StringComparison.Ordinal); + } + } + + [Fact] + public void TagsSupportsImmutableSortedSets() + { + var document = new OpenApiDocument + { + Tags = ImmutableSortedSet.Create( + new DescendingOpenApiTagComparer(), + [ + new OpenApiTag { Name = "tagB" }, + new OpenApiTag { Name = "tagA" }, + new OpenApiTag { Name = "tagC" }, + ]), + }; + + var names = document.Tags.Select(t => t.Name); + Assert.Equal(["tagC", "tagB", "tagA"], names); + Assert.IsType>(document.Tags); + } + public static TheoryData OpenApiSpecVersions() { var values = new TheoryData();