Swashbuckle.aspnetcore: TagActionsBy custom tag or controller name

Created on 18 Aug 2017  Â·  12Comments  Â·  Source: domaindrivendev/Swashbuckle.AspNetCore

Hi,

I was looking for a solution where I could group the actions of one or more controllers under a custom tag. I did a bit of searching round and found this section in the documentation. I dug into it and managed to produce a solution where I assign a custom attribute to controller(s) that I want to be tagged together. I then created an extension method for the ApiDescription class that will either extract the name in my custom attribute or use the name of the controller if the attribute is not present. This has worked, however, it was a little painful getting the name of the controller when my attribute was not present. I ended up having to duplicate some of the code in the ApiDescriptionExtensions class. Ideally, I would have just used the ControllerName extension method but this has been marked as internal.

I have two questions:

  1. Is there a reason why the ControllerName extension method is marked as internal? If not, would it be possible to make it public?
  2. Is there an alternative approach to what I have done which achieves the same result?

Here is the code that I have produced to achieve this.

public class SwaggerGroupAttribute : Attribute
{
    public string GroupName { get; }

    public SwaggerGroupAttribute(string groupName)
    {
        GroupName = groupName;
    }
}

public static class ApiDescriptionExtensions
{
    public static string GroupBySwaggerGroupAttribute(this ApiDescription api)
    {
        var groupNameAttribute = (SwaggerGroupAttribute)api.ControllerAttributes().SingleOrDefault(attribute => attribute is SwaggerGroupAttribute);

        // ------
        // Lifted from ApiDescriptionExtensions
        var actionDescriptor = api.GetProperty<ControllerActionDescriptor>();

        if (actionDescriptor == null)
        {
            actionDescriptor = api.ActionDescriptor as ControllerActionDescriptor;
            api.SetProperty(actionDescriptor);
        }
        // ------

        return groupNameAttribute != null ? groupNameAttribute.GroupName : actionDescriptor?.ControllerName;
    }
}

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSwaggerGen(c => 
    {
        // ...
        c.TagActionsBy(api => api.GroupBySwaggerGroupAttribute());
    }
}

// ******
// Example usage
[SwaggerGroup(“Grouped”)]
public class GroupedAController : Controller { }

[SwaggerGroup(“Grouped”)]
public class GroupedBController : Controller { }

public class NonGroupedController : Controller { }

public class AnotherController : Controller { }

// This results in three groups on the swagger page
//
// - Grouped
// - NonGrouped
// - Another


Most helpful comment

I'm using 5.0.0-beta and got the following to work:

    ...
    [ApiExplorerSettings(GroupName = "Values")]
    public class ValuesController : ControllerBase
    {
    ...
    c.OperationFilter<TagByApiExplorerSettingsOperationFilter>();
public class TagByApiExplorerSettingsOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
        {
            var apiExplorerSettings = controllerActionDescriptor
                .ControllerTypeInfo.GetCustomAttributes(typeof(ApiExplorerSettingsAttribute), true)
                .Cast<ApiExplorerSettingsAttribute>().FirstOrDefault();
            if (apiExplorerSettings != null && !string.IsNullOrWhiteSpace(apiExplorerSettings.GroupName))
            {
                operation.Tags = new List<OpenApiTag> {new OpenApiTag {Name = apiExplorerSettings.GroupName}};
            }
            else
            {
                operation.Tags = new List<OpenApiTag>
                    {new OpenApiTag {Name = controllerActionDescriptor.ControllerName}};
            }
        }
    }
}

All 12 comments

There's a built-in _SwaggerOperationAttribute_ that should allow you override the default tags for any given action

Thanks for the reply @domaindrivendev. My understanding is that the _SwaggerOperationAttribute_ can only be applied to actions. I was looking for a way to do it at the controller level. Is there an equivalent attribute that I can put onto the controller?

I was hoping to use the ControllerName as well today, +1 for making it public readonly

For reference, I am now doing:

    [ApiVersion("1.0")]
    [AdvertiseApiVersions("1.0")]
    [ApiRoute("dienstverleningen")]
    [ApiExplorerSettings(GroupName = "Dienstverleningen")]
    public class PublicServiceController : ApiBusController
    {
x.OperationFilter<TagByApiExplorerSettingsOperationFilter>();
public class TagByApiExplorerSettingsOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var apiGroupNames = context
                .ApiDescription
                .ControllerAttributes()
                .OfType<ApiExplorerSettingsAttribute>()
                .Where(x => !x.IgnoreApi)
                .Select(x => x.GroupName)
                .ToList();

            if (apiGroupNames.Count == 0)
                return;

            var tags = operation.Tags?.Select(x => x).ToList() ?? new List<string>();

            var controllerDescriptor = context.ApiDescription.GetProperty<ControllerActionDescriptor>();
            if (controllerDescriptor != null)
                tags.Remove(controllerDescriptor.ControllerName);

            foreach (var apiGroupName in apiGroupNames)
                if (!tags.Contains(apiGroupName))
                    tags.Add(apiGroupName);

            operation.Tags = tags;
        }

+1

@CumpsD I cann't found operation.Tags in v2.2.0.

For anyone that needs it, v2.5.0 deprecates some ApiDescription extensions which make the TagByApiExplorerSettingsOperationFilter above not work properly. Here's what is should look like with v2.5.0:

public class TagByApiExplorerSettingsOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var apiGroupNames = context
                .ControllerActionDescriptor
                .GetControllerAndActionAttributes(true)
                .OfType<ApiExplorerSettingsAttribute>()
                .Where(x => !x.IgnoreApi)
                .Select(x => x.GroupName)
                .ToList();

            if (apiGroupNames.Count == 0)
                return;

            var tags = operation.Tags?.Select(x => x).ToList() ?? new List<string>();

            var controllerDescriptor = context.ControllerActionDescriptor;
            if (controllerDescriptor != null)
                tags.Remove(controllerDescriptor.ControllerName);

            foreach (var apiGroupName in apiGroupNames)
                if (!tags.Contains(apiGroupName))
                    tags.Add(apiGroupName);

            operation.Tags = tags;
        }
    }

Another option is to use the [ApiExplorerSettings(GroupName = "Group")] on your controllers and then replace the default DocInclusionPredicate and TagActionsBy. Something like this:

services.AddSwaggerGen(options =>
{
    options.SwaggerDoc(version,
        new Info
        {
            Title = name,
            Version = version
        }
    );

    options.DocInclusionPredicate((_, api) => !string.IsNullOrWhiteSpace(api.GroupName));

    options.TagActionsBy(api => api.GroupName);
});

In the output swagger, tags is an array. So how would I attach a second tag in some cases?

Ideally I'd like to do a custom SwaggerTag attribute, like
public class SomeController : Controller { [SwaggerTag("someCategory")] public StatusCodeResult Get() { ..... } }

the options.TagActionsBy doesn't seem to be right for this, it returns a string for 1 tag, not a collection of strings.

I'm using 5.0.0-beta and got the following to work:

    ...
    [ApiExplorerSettings(GroupName = "Values")]
    public class ValuesController : ControllerBase
    {
    ...
    c.OperationFilter<TagByApiExplorerSettingsOperationFilter>();
public class TagByApiExplorerSettingsOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
        {
            var apiExplorerSettings = controllerActionDescriptor
                .ControllerTypeInfo.GetCustomAttributes(typeof(ApiExplorerSettingsAttribute), true)
                .Cast<ApiExplorerSettingsAttribute>().FirstOrDefault();
            if (apiExplorerSettings != null && !string.IsNullOrWhiteSpace(apiExplorerSettings.GroupName))
            {
                operation.Tags = new List<OpenApiTag> {new OpenApiTag {Name = apiExplorerSettings.GroupName}};
            }
            else
            {
                operation.Tags = new List<OpenApiTag>
                    {new OpenApiTag {Name = controllerActionDescriptor.ControllerName}};
            }
        }
    }
}

5.0.0-beta now includes an TagActionsBy overload that supports returning an array of tags. This should allow for the above customizations to be simplified

@domaindrivendev custom tags and tag descriptions (from controller xml docs) do not play well together. do you have any nice workaround in mind where I can link a custom tag to a controller's description?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rgelb picture rgelb  Â·  3Comments

JoelAdamWeiss picture JoelAdamWeiss  Â·  4Comments

m-demydiuk picture m-demydiuk  Â·  3Comments

engelhardtda picture engelhardtda  Â·  3Comments

michael-x picture michael-x  Â·  3Comments