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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[SwaggerControllerOrder(0)] | |
public class FooController : ApiController | |
{ | |
// ... | |
} | |
[SwaggerControllerOrder(10)] | |
public class BarController : ApiController | |
{ | |
// ... | |
} | |
[SwaggerControllerOrder(10)] | |
public class BazController : ApiController | |
{ | |
// ... | |
} |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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<TargetException>"/> 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<TargetException>"/> 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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
I have followed the same but not working
ReplyDeleteAt least define "Not working". I can't help you with this bare information.
DeleteI am using DocumentFilter showinswaggerattribute to show the required API controllers in swagger.
DeleteAfter that i am using the "c.OrderActionGroupsBy(new SwaggerControllerOrderComparer(Assembly.GetExecutingAssembly()))"
Will it be causing any problem?
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 )
DeleteHi Rob,
ReplyDeleteIf 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
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?
ReplyDeleteI'm having this error on the WebApiConfig file:
ReplyDelete'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.
This a very old post; if you're using the latest Swashbuckle it's likely this won't work anymore without some changes.
Delete