2018-08-16

Swashbuckle custom ordering of controllers

By default, Swagger (or rather: Swashbuckle) orders the documentation of controllers by controllername. So if you have a FooController, BarController and BazController they will be ordered as Bar, Baz, Foo in the swagger UI. Let's change that.

Swashbuckle offers an option to order the "ActionGroups" as these are called using the OrderActionGroupsBy method. However, this method expects an IComparer<string> which doesn't offer much flexibility by itself other than ordering alphabetically in ascending or descending order. There's not much more creative we can get with this rather simple interface. Or... can we?

I was hoping to be able to add some attributes to my controllers to set the order manually but couldn't find any possibilities to do so, so I implemented it. The idea is simple: we'd like to have an attribute we can slap on a controller to influence the order the controller should be documented in. That's about it.

So, first we will need to implement our attribute:

/// <summary>
/// Annotates a controller with a Swagger sorting order that is used when generating the Swagger documentation to
/// order the controllers in a specific desired order.
/// </summary>
public class SwaggerControllerOrderAttribute : Attribute
{
/// <summary>
/// Gets the sorting order of the controller.
/// </summary>
public int Order { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerControllerOrderAttribute"/> class.
/// </summary>
/// <param name="order">Sets the sorting order of the controller.</param>
public SwaggerControllerOrderAttribute(int order)
{
Order = order;
}
}
Now we can annotate our controllers with this attribute:

[SwaggerControllerOrder(0)]
public class FooController : ApiController
{
// ...
}
[SwaggerControllerOrder(10)]
public class BarController : ApiController
{
// ...
}
[SwaggerControllerOrder(10)]
public class BazController : ApiController
{
// ...
}
Now, we need a class that implements IComparer<string> but somehow, magically, knows how to get to the attributes on the controllers. The plan is as follows: we create a dictionary with controllersnames and the desired order as key/value pairs. We keep a reference to this dictionary around. When the Compare(string x, string y) method is called we first check our dictionary to see if we have a specific order for controller X and/or controller Y and compare them. If the orders are equal we fall back to comparing the names; in all other cases we simply return the result of the comparison of the order values. The question is: how do we populate this dictionary?

Well, there are 2 options actually. First, in our SwaggerControllerOrderComparer, as we'll call it, we could use some reflection to figure out all controllers in the assembly. That works quite nicely and is the simplest to use. However, another option would be that the calling code provides the controllers we want to sort using this method so we have better control of how the assembly is scanned for controllers. This resulted in a SwaggerControllerOrderComparer with two constructors; one which accepts the assembly to scan for controllers and one which accepts an IEnumerable<Type> that can be used to pass in specific controllers.

/// <summary>
/// Represents a controller name comparison operation that uses specific rules to determine the sort order of a
/// controller when generating Swagger documentation.
/// </summary>
/// <typeparam name="T">The type controllers should implement (e.g. "ApiController")</typeparam>
public class SwaggerControllerOrderComparer<T> : IComparer<string>
{
private readonly Dictionary<string, int> _orders; // Our lookup table which contains controllername -> sortorder pairs
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerControllerOrderComparer&lt;TargetException&gt;"/> class.
/// </summary>
/// <param name="assembly">The assembly to scan for for classes implementing <typeparamref name="T"/>.</param>
public SwaggerControllerOrderComparer(Assembly assembly)
: this(GetFromAssembly<T>(assembly)) { }
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerControllerOrderComparer&lt;TargetException&gt;"/> class.
/// </summary>
/// <param name="controllers">
/// The controllers to scan for a <see cref="SwaggerControllerOrderAttribute"/> to determine the sortorder.
/// </param>
public SwaggerControllerOrderComparer(IEnumerable<Type> controllers)
{
// Initialize our dictionary; scan the given controllers for our custom attribute, read the Order property
// from the attribute and store it as controllername -> sorderorder pair in the (case-insensitive)
// dicationary.
_orders = new Dictionary<string, int>(
controllers.Where(c => c.GetCustomAttributes<SwaggerControllerOrderAttribute>().Any())
.Select(c => new { Name = ResolveControllerName(c.Name), c.GetCustomAttribute<SwaggerControllerOrderAttribute>().Order })
.ToDictionary(v => v.Name, v => v.Order), StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Returns all <typeparamref name="TController"/>'s from the given assembly.
/// </summary>
/// <typeparam name="TController">The type classes must implement to be regarded a controller.</typeparam>
/// <param name="assembly">The assembly to scan for given <typeparamref name="TController"/>s.</param>
/// <returns>Returns all types implementing <typeparamref name="TController"/>.</returns>
public static IEnumerable<Type> GetFromAssembly<TController>(Assembly assembly)
{
return assembly.GetTypes().Where(c => typeof(TController).IsAssignableFrom(c));
}
/// <summary>
/// Compares to specified controller names and returns an integer that indicates their relative position in the
/// sort order.
/// </summary>
/// <param name="controllerX">The first controller name to compare.</param>
/// <param name="controllerY">The second controller name to compare.</param>
/// <returns>
/// A 32-bit signed integer that indicates the relationship between the two controller positions. If
/// controllerX precedes controllerY a value less than zero is returned. When controllerY precedes controllerX
/// a value greater than zero is returned. When controllerX and controllerY occur in the same sort order the
/// value zero is returned.
/// </returns>
public int Compare(string controllerX, string controllerY)
{
// Try, for both controllers, to get the sortorder value from our lookup; if none is found, assume int.MaxValue
if (!_orders.TryGetValue(controllerX, out int xOrder))
xOrder = int.MaxValue;
if (!_orders.TryGetValue(controllerY, out int yOrder))
yOrder = int.MaxValue;
// If sortorder values differ, return the result
if (xOrder != yOrder)
return xOrder.CompareTo(yOrder);
// If sort order values are equal, we fall back to ordering by name
return string.Compare(controllerX, controllerY, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines the 'friendly' name of the controller by stripping the (by convention) "Controller" suffix
/// from the name. If there's a built-in way to do this in .Net then I'd love to hear about it!
/// </summary>
/// <param name="name">The name of the controller.</param>
/// <returns>The friendly name of the controller.</returns>
private static string ResolveControllerName(string name)
{
const string suffix = "Controller"; // We want to strip "Controller" from "FooController"
// Ensure name ends with suffix (case-insensitive)
if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
// Return name with suffix stripped
return name.Substring(0, name.Length - suffix.Length);
// Suffix not found, return name as-is
return name;
}
}
With that out of the way, all we need to do is tell Swashbuckle to use our comparer:

public static class WebApiConfig
{
public static void Register(HttpConfiguration httpConfiguration)
{
// TODO: Add any additional configuration code.
httpConfiguration
.EnableSwagger(c => {
c.OrderActionGroupsBy(new SwaggerControllerOrderComparer<ApiController>(Assembly.GetExecutingAssembly()));
})
.EnableSwaggerUi();
}
}
view raw WebApiConfig.cs hosted with ❤ by GitHub
And that's it! Enjoy your custom ordered controllers!

8 comments:

  1. I have followed the same but not working

    ReplyDelete
    Replies
    1. At least define "Not working". I can't help you with this bare information.

      Delete
    2. I am using DocumentFilter showinswaggerattribute to show the required API controllers in swagger.
      After that i am using the "c.OrderActionGroupsBy(new SwaggerControllerOrderComparer(Assembly.GetExecutingAssembly()))"
      Will it be causing any problem?

      Delete
    3. With Latest Swagger code, Change the Controller File Name as per order of controller you want. And Swagger UI will list Controller as per File Name. ( only exception i saw Health check Controller. It always display at bottom in Swagger UI )

      Delete
  2. Hi Rob,
    If i use the DocumentFilter option this order is not working.
    If i dont use the documentFilter then c.OrderActionGroupsBy working fine and getting cutom order.
    Please help me how to use c.OrderActionGroupsBy along with c.DocumentFilter

    ReplyDelete
  3. This not working seems like it is a bug in swagger-ui. The documentation file is generated in the correct order but is always displayed alphabetically in swagger-ui. I wrote a reversing comparer and it had the same result. Or should I configure something in swagger-ui that I'm not aware of?

    ReplyDelete
  4. I'm having this error on the WebApiConfig file:

    'HttpConfiguration' does not contain a definition for 'EnableSwagger' and no accessible extension method 'EnableSwagger' accepting a first argument of type 'HttpConfiguration' could be found (are you missing a using directive or an assembly reference?).

    I'm creating all the files on the root of the project.

    ReplyDelete
    Replies
    1. This a very old post; if you're using the latest Swashbuckle it's likely this won't work anymore without some changes.

      Delete