14

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?

lpacheco
  • 976
  • 1
  • 14
  • 27

4 Answers4

19

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>();
Alex
  • 7,901
  • 1
  • 41
  • 56
antmeehan
  • 865
  • 9
  • 11
  • 1
    Can you change the typos from `swaggerDoc.paths` to `swaggerDoc.Paths`. – Alex Jul 27 '19 at 16:23
  • This approach works, but I suggest this instead: `swaggerDoc.paths = new SortedDictionary(swaggerDoc.paths);` because Dictionary does not guarantee the order of enumeration (and may not even be in-order if the internal hash table grows). Also `paths` is correct and `Paths` is wrong (at least on Swashbuckle 5.6 for Framework). – Nick Mar 03 '20 at 20:55
  • "Swagger.paths" is the correct assembly namespace. – AV2000 Dec 09 '20 at 00:39
  • I think it was a typo in the API. 5.6.3 is currently accepting `swaggerDoc.Paths` – tgarcia Jan 25 '21 at 20:05
  • Just pushed [this gist](https://gist.github.com/thiagosgarcia/8cd1615f69d3bcfc82c1735bcdab7223) based on your answer. – tgarcia Jan 25 '21 at 20:06
17

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 :)

Kaitiff
  • 416
  • 3
  • 9
  • A string array can also be used to determine the order of HttpMethods by using `c.OrderActionsBy(apiDesc =>$"apiDesc.ActionDescriptor.RouteValues["controller"]}_{Array.IndexOf(methodsOrder, apiDesc.HttpMethod.ToLower())}");` – filtercoffee Jun 16 '20 at 20:49
  • 1
    There is a typo in the previous comment (missing a curly braces)... `c.OrderActionsBy(apiDesc => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{Array.IndexOf(methodsOrder, apiDesc.HttpMethod.ToLower())}");` along with the failure to mention prior to the services.AddSwaggerGen line of code you need to define the array. i.e. `string[] methodsOrder = new string[5] { "get", "post", "put", "patch", "delete", "options", "trace" };` – Terence Golla Dec 17 '20 at 01:53
  • 3
    Another useful sort: c.OrderActionsBy((apiDesc) => $"{apiDesc.RelativePath}_{apiDesc.HttpMethod}"); – Daniel May 19 '21 at 18:10
  • 1
    Another I like is: c.OrderActionsBy(apiDesc => $"{apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.RelativePath}_{apiDesc.HttpMethod}"); – JMIII Dec 09 '22 at 20:11
12

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)
{
   ...
}
dkokkinos
  • 361
  • 2
  • 9
  • Works like a charm, thanks. Quick one, for better useability, I have removed the exception if the orderAttribute is absent (null) and replaced 'int order = orderAttribute.Order;' by 'int order = orderAttribute?.Order ?? 1000;'. So, not placing the attribute on a method just send it at the bottom instead of crashing. – XavierAM Feb 23 '21 at 18:37
0

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:

    1. your default namespace (i.e. 'Your.Default.Namespace')
    2. the sub-path of your resource (i.e. 'Resources')
    3. the filename of your resource (i.e. 'Swagger_Custom_index.html')

    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);
    
Jpsy
  • 20,077
  • 7
  • 118
  • 115