-
Notifications
You must be signed in to change notification settings - Fork 282
Description
Describe the bug
In the v1 API, OpenApiDocument.Tags was an IList<OpenApiTag>, enabling us to order the tags however we want (e.g., in a Swashbuckle document filter). In the v2 API, the property is an ISet<OpenApiTag>, which, while more difficult, would let us use a custom ordering via SortedSet<OpenApiTag> with a custom comparer. However, the OpenApiDocument.Tags setter doesn't actually preserve the collection you give it, unless it's specifically a HashSet<OpenApiTag> that uses OpenApiTagComparer.
Technically, fully populating a SortedSet<OpenApiTag> before assigning it to OpenApiDocument.Tags often "works", because the current HashSet implementation will enumerate in the same order as insertion in many cases (I think it's if there aren't any hash code collisions and you don't remove any elements and add more). But initializing OpenApiDocument.Tags to a SortedSet<OpenApiTag> and then populating it doesn't produce the desired result.
OpenApi File To Reproduce
Any document with multiple tags. For example:
{
"openapi": "3.0.4",
"tags": [
{
"name": "Data",
"description": "..."
},
{
"name": "Export",
"description": "..."
},
{
"name": "Tables",
"description": "..."
},
{
"name": "Versioning",
"description": "..."
}
]
}Expected behavior
I can order the document's tags such that the serialized document maintains the order I want instead of alphabetic:
{
"openapi": "3.0.4",
"tags": [
{
"name": "Versioning",
"description": "..."
},
{
"name": "Tables",
"description": "..."
},
{
"name": "Data",
"description": "..."
},
{
"name": "Export",
"description": "..."
}
]
}Screenshots/Code Snippets
Swashbuckle document filter that works with the v1 API:
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var tagList = new SortedList<int, OpenApiTag>();
// The actual code discovers the tags using the ApiExplorer
tagList.Add(2, new OpenApiTag { Name = "Data" });
tagList.Add(3, new OpenApiTag { Name = "Export" });
tagList.Add(1, new OpenApiTag { Name = "Tables" });
tagList.Add(0, new OpenApiTag { Name = "Versioning" });
swaggerDoc.Tags = tagList.Values;
}Code that "works" in v2, assuming the HashSet cooperates:
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var tagSet = new SortedSet<OpenApiTag>(new OrderedOpenApiTagComparer());
tagSet.Add(new OrderedOpenApiTag(2) { Name = "Data" });
tagSet.Add(new OrderedOpenApiTag(3) { Name = "Export" });
tagSet.Add(new OrderedOpenApiTag(1) { Name = "Tables" });
tagSet.Add(new OrderedOpenApiTag(0) { Name = "Versioning" });
swaggerDoc.Tags = tagSet; // The setter copies the tags into a new HashSet
}
private sealed class OrderedOpenApiTag(int order) : OpenApiTag
{
public int Order { get; } = order;
}
private sealed class OrderedOpenApiTagComparer : IComparer<OpenApiTag>
{
public int Compare(OpenApiTag? x, OpenApiTag? y)
{
var xOrder = (x as OrderedOpenApiTag)?.Order ?? -1;
var yOrder = (y as OrderedOpenApiTag)?.Order ?? -1;
return xOrder.CompareTo(yOrder);
}
}Additional context
Perhaps the property setter should only override the ISet implementation if given a HashSet that uses the default comparer and/or an empty set?