Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TagActionsBy custom tag or controller name #467

Closed
djwales opened this issue Aug 18, 2017 · 20 comments
Closed

TagActionsBy custom tag or controller name #467

djwales opened this issue Aug 18, 2017 · 20 comments

Comments

@djwales
Copy link

djwales commented Aug 18, 2017

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

  
@domaindrivendev
Copy link
Owner

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

@djwales
Copy link
Author

djwales commented Aug 21, 2017

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?

@CumpsD
Copy link

CumpsD commented Nov 12, 2017

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

@CumpsD
Copy link

CumpsD commented Nov 12, 2017

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;
        }

@RainingNight
Copy link

+1

@RainingNight
Copy link

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

@falvarez1
Copy link

falvarez1 commented Jun 7, 2018

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;
        }
    }

@eugenpodaru
Copy link

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);
});

@AnthonySteele
Copy link

AnthonySteele commented Jun 28, 2018

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.

@kiwipiet
Copy link

kiwipiet commented Feb 9, 2019

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}};
            }
        }
    }
}

@domaindrivendev
Copy link
Owner

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

@ckarcz
Copy link

ckarcz commented Jan 16, 2020

@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?

@Moseyza
Copy link

Moseyza commented Sep 12, 2021

By default Tags array in json output of swashbuckle is made by XmlCommentsDocumentFilter. For changing this behavior is there any way to disable this filter and user our own filter instead?

@enriquecorp
Copy link

Actually, in .net6 I'm using TagsAttribute
image

@apetrut
Copy link

apetrut commented Jul 14, 2022

@enriquecorp Tags attribute works great, but how can I add a description for that tag?

@yingxinj
Copy link

yingxinj commented Dec 6, 2022

@apetrut and others - my linked PR fixes the issue where tags created using TagsAttribute do not play well with the description pulled from the controller XML docs, please let me know if this would be useful :)

@randyburden
Copy link

This solution just worked for me. Adapted from: https://stackoverflow.com/a/65067565/670028

Step 1: Add controller convention class

/// <summary>
/// Uses the [DisplayName] attribute as the Controller name in OpenAPI spec and Swagger/ReDoc UI if available else the default Controller name.
/// </summary>
public class ControllerDocumentationConvention : IControllerModelConvention
{
    void IControllerModelConvention.Apply(ControllerModel controller)
    {
        if (controller == null)
        {
            return;
        }
        
        foreach (var attribute in controller.Attributes)
        {
            if (attribute is DisplayNameAttribute displayNameAttribute && !string.IsNullOrWhiteSpace(displayNameAttribute.DisplayName))
            {
                controller.ControllerName = displayNameAttribute.DisplayName;
            }
        }
    }
}

Step 2: Add convention to startup

services.AddControllers(o =>
{
   o.Conventions.Add(new ControllerDocumentationConvention());
});

Step 3: Add [DisplayName] attribute to any controller that needs a more user friendly name

@DenKn
Copy link

DenKn commented Nov 27, 2023

@randyburden did a good solution but if you use tags you can do it

public class ControllerDocumentationConvention : IControllerModelConvention
{
    void IControllerModelConvention.Apply(ControllerModel controller)
    {
        foreach (var attribute in controller.Attributes)
        {
            if (attribute is TagsAttribute tagsAttribute && tagsAttribute.Tags.Any())
            {
                controller.ControllerName = tagsAttribute.Tags.First();
            }
        }
    }
}

But there is one trouble - your api has changed. So, we are waiting for
#2565

@DenKn
Copy link

DenKn commented Nov 29, 2023

I looked at commit 2565 and wrote such class in my code.
Everything works correctly.

public class ControllerTagWithDescriptionFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var controllers = context.ApiDescriptions
            .Select(apiDesc => apiDesc.ActionDescriptor as ControllerActionDescriptor)
            .Where(actionDescriptor => actionDescriptor != null)
            .GroupBy(filteredActionDescriptor => filteredActionDescriptor?.ControllerName ?? string.Empty)
            .Select(group => new KeyValuePair<string, ControllerInfo>(group.Key, GetControllerInfo(group)))
            .ToList();

        var openApiTags = new List<OpenApiTag>();

        foreach (var (controllerName, controllerInfo) in controllers)
        {
            OpenApiTag? tag = swaggerDoc.Tags.FirstOrDefault(tag => controllerName == tag.Name ||
                                                                    controllerInfo.CustomTagName == tag.Name);
            if (tag != null)
            {
                openApiTags.Add(new OpenApiTag()
                {
                    Name = !controllerInfo.CustomTagName.IsNullOrEmpty() ? controllerInfo.CustomTagName : controllerName,
                    Description = tag?.Description
                });
            }
        }

        swaggerDoc.Tags = openApiTags.OrderBy(tag => tag.Name).ToList();
    }
    
    private class ControllerInfo
    {
        public Type? ControllerType { get; set; }
        public string? CustomTagName { get; set; }
    }

    private static ControllerInfo GetControllerInfo(IGrouping<string, ControllerActionDescriptor> group)
    {
        string? customTagName = group.First().MethodInfo.DeclaringType?.GetCustomAttribute<TagsAttribute>()?.Tags[0];

        if (customTagName == null)
        {
            // if we have controller inheritance
            var firstOrDefault = group.First().EndpointMetadata
                .OfType<TagsAttribute>().FirstOrDefault();
            customTagName = firstOrDefault != null ? firstOrDefault.Tags[0] : string.Empty;
        }
        
        var controllerInfo = new ControllerInfo
        {
            ControllerType = group.First().ControllerTypeInfo.AsType(),
            CustomTagName = customTagName
        };

        return controllerInfo;
    }
}

@IlyaBelitser
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests