lpacheco
lpacheco

Reputation: 1026

How can I change order the operations are listed in a group in Swashbuckle?

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

Answers (5)

Turtle
Turtle

Reputation: 106

Overview

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.

Explanation

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.

IDocumentFilter

    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

antmeehan
antmeehan

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

dkokkinos
dkokkinos

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

Kaitiff
Kaitiff

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

Jpsy
Jpsy

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:

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

Upvotes: 0

Related Questions