Remove a route with IOperationFilter in SwashBuckle
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?
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)