10

I am looking for a way to show/hide WebAPI routes in the Swagger documentation using SwashBuckle in a configurable way. Adding [ApiExplorerSettings(IgnoreApi = true)] will indeed hide the routes but I'd need to recompile every time I want that to change.

I have looked into creating an IOperationFilter to work with a custom Attribute that I defined. That way I can decorate the routes with a [SwaggerTag("MobileOnly")] and check the web.config or something to see if the route should be shown. The Attribute is defined as such:

public class SwaggerTagAttribute : Attribute
{
    public string[] Tags { get; private set; }

    public SwaggerTagAttribute(params string[] tags)
    {
        this.Tags = tags;
    }
}

The IOperationFilter that detects the attribute is defined and the IDocumentFilter that removes the path is defined here:

public class RemoveTaggedOperationsFilter : IOperationFilter, IDocumentFilter
{
    private List<string> TagsToHide;

    public RemoveTaggedOperationsFilter()
    {
        TagsToHide = ConfigurationManager.AppSettings["TagsToHide"].Split(',').ToList();
    }

    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var tags = apiDescription.ActionDescriptor
            .GetCustomAttributes<SwaggerTagAttribute>()
            .Select(t => t.Tags)
            .FirstOrDefault();

        if (tags != null && TagsToHide.Intersect(tags).Any())
        {
            operation.tags = new List<string> {"Remove Me "};
        }
    }

    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {
        foreach (var value in swaggerDoc.paths.Values)
        {
            if (value.post != null && value.post.tags.Contains("Remove Me"))
                value.post = null;

            if (value.get != null && value.get.tags.Contains("Remove Me"))
                value.get = null;

            if (value.put != null && value.put.tags.Contains("Remove Me"))
                value.put = null;

            if (value.delete != null && value.delete.tags.Contains("Remove Me"))
                value.delete = null;
        }
    }
}

And registered as such:

 GlobalConfiguration.Configuration
            .EnableSwagger(c =>
                {
                    c.OperationFilter<RemoveTaggedOperationsFilter>();
                    c.DocumentFilter<RemoveTaggedOperationsFilter>();
                });

I feel that this is inefficient and hacky to tag something for removal later when I have access to it earlier. Is there any way that I just remove the route from the within IOperationFilter.Apply rather than wait for the IDocumentFilter and scan through it?

LukeP
  • 1,505
  • 1
  • 16
  • 25

1 Answers1

7

Someone had posted an answer earlier and said they'd post code once they got a chance. They have deleted their answer for some reason but it got me to a better solution.

Rather than using IOperationFilter to tag the route and then IDocumentFilter to remove the route later you can just use IDocumentFilter to find the custom attribute and remove it in one fell swoop. The code is below:

public class HideTaggedOperationsFilter : IDocumentFilter
{
    private List<string> TagsToHide;

    public HideTaggedOperationsFilter()
    {
        TagsToHide = ConfigurationManager.AppSettings["TagsToHide"].Split(',').ToList();
    }

    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {
        if (_tagsToHide == null) return;

        foreach (var apiDescription in apiExplorer.ApiDescriptions)
        {
            var tags = apiDescription.ActionDescriptor
                .GetCustomAttributes<SwaggerTagAttribute>()
                .Select(t => t.Tags)
                .FirstOrDefault();

            if (tags == null || !_tagsToHide.Intersect(tags).Any())
                continue;

            var route = "/" + apiDescription.Route.RouteTemplate.TrimEnd('/');
            swaggerDoc.paths.Remove(route);
        }
    }
}

public class SwaggerTagAttribute : Attribute
{
    public string[] Tags { get; }

    public SwaggerTagAttribute(params string[] tags)
    {
        this.Tags = tags;
    }
}

Register the IDocumentFilter:

GlobalConfiguration.Configuration.EnableSwagger(c =>
{
    ...
    c.DocumentFilter<HideTaggedOperationsFilter>();
});

Then just decorate a route like so:

 [SwaggerTag("MobileOnly")]
 public IHttpActionResult SendTest(Guid userId)
 {
    return OK();
 }

Edit: There are some issue posts on the GitHub page for SwashBuckle that recommend setting each of the HTTP verbs to null on the swaggerDoc.path in Apply. I found this to break a lot of auto code generators like AutoRest so I am simply removing the path as a whole. (It looks more succinct too)

LukeP
  • 1,505
  • 1
  • 16
  • 25
  • 2
    This approach will not work if you have the same route with different verbs, but only want to disable some. Ie having GET /transcations and POST /transactions, both are removed when only 1 action method is decorated with the custom attribute. You would need to also take the http method into consideration. – Para Jaco Jun 24 '16 at 10:27
  • @ParaJaco that's a great point. It probably wouldn't be too difficult to extend it to take the HTTP verbs into account. – LukeP Jun 24 '16 at 16:10
  • Could I decorate the whole controller ? – Rbacarin Dec 13 '16 at 18:14
  • Good question. I'm not 100% sure as I have not tried it. Feel free to give it a try and let me know. If it doesn't work maybe I can see what would need to be done to extend it to be able to support controller attribute decoration. – LukeP Dec 14 '16 at 06:45