Reputation: 1026
I'm using Swashbuckle to generate Swagger UI. It has options for choosing a grouping key (controller by default) and the ordering of the groups, but I would like to choose an order for the operations in a group so that GET
appears always before DELETE
for example.
I've found how to implement document filters and I can get and order ApiDescriptions
by HttpMethod
, but changing the order in ApiDescriptions
doesn't reflect in the generated Swagger UI and I can't find how to persist the order in swaggerDoc
.
SwaggerDocument
has a paths
property, but the PathItem
in it has each HTTP method as a property, so I can't figure how to choose an order of presentation for them. Eventough, when the Swagger UI for my API is generated, different controllers get different method order in the page.
Should I manually reorder the methods implementation in my controller instead?
Upvotes: 15
Views: 29918
Reputation: 106
You can exert some limited control over the ordering from C#, but you can only influence the generated OpenAPI specification JSON. If you're not familiar with how that works, trying to sort it leads to all kinds of surprising behavior. If you need full control, you must do it in the browser as outlined by Jpsy.
The OpenAPI specification groups endpoints by path. The following example will have one expandable category My Resources
in Swagger UI, containing two operations on the same path.
{
"openapi": "3.0.1",
"paths": {
"/api/v1/my-resources": {
"get": {
"tags": [
"My Resources"
],
"operationId": "Get all my resources"
},
"post": {
"tags": [
"My Resources"
],
"operationId": "Create a resource"
}
}
}
}
Swagger UI does not apply any sorting to the operations. All operations will be shown in the order given by the OpenAPI definition JSON. The tag categories also appear in the order that the tags are discovered in the JSON.
So any solution in C# will be unable to free operations from their path groups, or sort the contents for each tag specifically.
For simple use cases, I find a document filter like below to be sufficient. The provided code sorts the path groups alphabetically and then each operation by method.
For more advanced use cases, you can use OpenAPI extensions to pass arbitrary data in the JSON which can be used for sorting in the browser.
public class OperationOrderingFilter : IDocumentFilter
{
public void Apply(OpenApiDocument document, DocumentFilterContext context)
{
// Order path groups (OpenApiPathItems) alphabetically
var pathKvps = document.Paths
.OrderBy(pathKvp =>
{
return pathKvp.Key;
})
.ToList();
document.Paths.Clear();
pathKvps.ForEach(kvp => document.Paths.Add(kvp.Key, kvp.Value));
// Then order operations by method within each group
document.Paths.ToList().ForEach(pathKvp =>
{
var operationKvps = pathKvp.Value.Operations
.OrderBy(kvp =>
{
var weight = kvp.Key switch
{
OperationType.Get => 10,
OperationType.Post => 20,
OperationType.Put => 30,
OperationType.Delete => 40,
_ => 50,
};
return weight;
})
.ToList();
pathKvp.Value.Operations.Clear();
operationKvps.ForEach(operationKvp =>
{
pathKvp.Value.Operations.Add(operationKvp);
});
});
}
}
Register the filter like so
builder.Services.AddSwaggerGen(options =>
{
options.DocumentFilter<OperationOrderingFilter>();
};
Upvotes: 0
Reputation: 933
Have you seen this issue? https://github.com/domaindrivendev/Swashbuckle/issues/11
public class CustomDocumentFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer)
{
//make operations alphabetic
var paths = swaggerDoc.Paths.OrderBy(e => e.Key).ToList();
swaggerDoc.Paths = paths.ToDictionary(e => e.Key, e => e.Value);
}
}
and then:
c.DocumentFilter<CustomDocumentFilter>();
Upvotes: 19
Reputation: 411
In order to order the Operations of controller in swagger OpenApi paths json spec you could create a custom Attribute OrderAttribute and then a IDocumentFilter which will reorder the OpenApiPaths.
public class OperationsOrderingFilter : IDocumentFilter
{
public void Apply(OpenApiDocument openApiDoc, DocumentFilterContext context)
{
Dictionary<KeyValuePair<string, OpenApiPathItem>,int> paths = new Dictionary<KeyValuePair<string, OpenApiPathItem>, int>();
foreach(var path in openApiDoc.Paths)
{
OperationOrderAttribute orderAttribute = context.ApiDescriptions.FirstOrDefault(x=>x.RelativePath.Replace("/", string.Empty)
.Equals( path.Key.Replace("/", string.Empty), StringComparison.InvariantCultureIgnoreCase))?
.ActionDescriptor?.EndpointMetadata?.FirstOrDefault(x=>x is OperationOrderAttribute) as OperationOrderAttribute;
if (orderAttribute == null)
throw new ArgumentNullException("there is no order for operation " + path.Key);
int order = orderAttribute.Order;
paths.Add(path, order);
}
var orderedPaths = paths.OrderBy(x => x.Value).ToList();
openApiDoc.Paths.Clear();
orderedPaths.ForEach(x => openApiDoc.Paths.Add(x.Key.Key, x.Key.Value));
}
}
then the attribute would be
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class OperationOrderAttribute : Attribute
{
public int Order { get; }
public OperationOrderAttribute(int order)
{
this.Order = order;
}
}
the registration of the filter in swagger would be
services.AddSwaggerGen(options =>
{
options.DocumentFilter<OperationsOrderingFilter>();
}
and an example of a controller method with the attribute would be:
[HttpGet]
[OperationOrder(2)]
[Route("api/get")]
public async Task<ActionResult> Get(string model)
{
...
}
Upvotes: 13
Reputation: 473
Had the same issue, and finally managed to fix it with the official doc. provided on this URL https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
services.AddSwaggerGen(c =>
{
...
c.OrderActionsBy((apiDesc) => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.HttpMethod}");
};
Is an easier and clearer path to solve it :)
Upvotes: 24
Reputation: 20882
You have to replace Swashbuckle's index.html with your own version and then add the "operationsSorter" config param within that file. Here are the steps for .NET Core 2.x. (.NET framework should only differ in the way an embedded resource is defined in your VS project).
Get a copy of SwashBuckle's original index.html from here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html
Place that copy in some sub-folder of your project.
You can choose a different file name, I chose:
\Resources\Swagger_Custom_index.html
Right-click that file, select 'Properties', select 'Configuration Properties' in left pane, under 'Advanced' in right pane find entry 'Build Action' and set it to 'Embedded resource'. Click Ok.
In Startup.cs add this block:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseSwaggerUI(c =>
{
c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("Your.Default.Namespace.Resources.Swagger_Custom_index.html");
});
app.UseMvc();
}
The identifier for the resource in the above GetManifestResourceStream
command is composed of:
All three parts are concatenated using dots (NO slashes or backslashes here).
If you don't use a sub-path but have your SwashBuckle index.html in root, just omit part 2.
Now edit Swagger_Custom_index.html and insert this block right before the line const ui = SwaggerUIBundle(configObject);
close to the end of the file:
// sort end points by verbs ("operations")
configObject.operationsSorter = (a, b) => {
var methodsOrder = ["get", "post", "put", "delete", "patch", "options", "trace"];
var result = methodsOrder.indexOf(a.get("method")) - methodsOrder.indexOf(b.get("method"));
if (result === 0) {
result = a.get("path").localeCompare(b.get("path"));
}
return result;
}
// Build a system
const ui = SwaggerUIBundle(configObject);
Upvotes: 0