Before jumping into a solution, let me qualify a few points:
- The YouTube video above and @abdusco are providing their own approach to API Versioning in their opinion, but the question is about ASP.NET Core API Versioning
- Although not specified, I'm going to assume you are not versioning by URL segment
- The default OpenAPI/Swagger UI only has a single pivot point (e.g. group) which you can select in a dropdown
- An OpenAPI/Swagger document must have unique URLs, which means you cannot have multiple APIs in the same document unless you version by URL segment (worst choice; least RESTful)
- By Swagger, I presume you mean Swashbuckle. The Swashbuckle OpenAPI generator uses
ApiDescription.GroupName
to collate APIs in a single document per group, which maps to the dropdown in UI. Changing this behavior is possible, but non-trivial.
- The default behavior of the API Explorer extensions for API Versioning sets the
ApiDescription.GroupName
to a formatted API version value if not otherwise set so that APIs are grouped by version. Prior to 4.1
, [ApiExplorerSettings(GroupName="...")]
was ignored and overwritten.
At the end of the day, you are asking for two grouping dimensions: category and API version. Unfortunately, the API Explorer, OpenAPI/Swagger UI, and Swashbuckle only intrinsically support one. Any form of hierarchy is not supported out-of-the-box. There a couple of ways you can get around this:
- Concatenate
group name
and API version
as a single dimension
- Create a Swashbuckle extension (ex: IOperationFilter) to add OpenAPI tags for a particular group. The UI can then be changed to add a second dropdown based on those tags for filtering or the tags can be used for collated groups (think Expander controls).
- Version by URL segment, produce a single OpenAPI document, update the
GroupName
back the way you want (This approach is not recommended)
There might be more options, but these are the most sensible. While it might not be what you're hoping for, given the limitations to work with, #3
is the most straight forward to implement. It would look like this:
public class SubgroupDescriptionProvider : IApiDescriptionProvider
{
private readonly IOptions<ApiExplorerOptions> options;
public SubgroupDescriptionProvider(IOptions<ApiExplorerOptions> options)
=> this.options = options;
// Execute after DefaultApiVersionDescriptionProvider.OnProvidersExecuted
public int Order => -1;
public void OnProvidersExecuting(ApiDescriptionProviderContext context) { }
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
{
var format = options.Value.GroupNameFormat;
var culture = CultureInfo.CurrentCulture;
var results = context.Results;
var newResults = new List<ApiDescription>(capacity: results.Count);
for (var i = 0; i < results.Count; i++)
{
var result = results[i];
var apiVersion = result.GetApiVersion();
var versionGroupName = apiVersion.ToString(format, culture);
// [ApiExplorerSettings(GroupName="...")] was NOT set so
// nothing else to do
if (result.GroupName == versionGroupName)
{
continue;
}
// must be using [ApiExplorerSettings(GroupName="...")] so
// concatenate it with the formatted API version
result.GroupName += " " + versionGroupName;
// optional: add version grouping as well
// note: this works because the api description will appear in
// multiple, but different, documents
var newResult = result.Clone();
newResult.GroupName = versionGroupName;
newResults.Add(newResult);
}
newResults.ForEach(results.Add);
}
}
You then register this with DI using:
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, SubgroupDescriptionProvider>());
If you use both sets of grouping, then you'll get multiple OpenAPI documents.
By API Version (default)
├─ v1
│ ├─ /utility
│ └─ /not-utility
└─ v2
├─ /utility
└─ /not-utility
By Group Name + API Version
├─ Utilities v1
│ └─ /utility
├─ Not Utilities v1
│ └─ /not-utility
├─ Utilities v2
│ └─ /utility
└─ Not Utilities v2
└─ /not-utility
If your versioning policy is something like N-2
, then the list should be reasonable. It's a product of group * version
, so the list will depend on how many groups and versions you ultimately have.
Having a true, two dimensional hierarchy should be technically feasible, but I've never done it nor known anyone willing to put the effort in to make it happen. The solution would be pretty specific your set of APIs.