Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static void EnableAnnotations(this SwaggerGenOptions options)
options.SchemaFilter<SwaggerSchemaAttributeFilter>();
options.OperationFilter<SwaggerResponseAttributeFilter>();
options.OperationFilter<SwaggerOperationAttributeFilter>();
options.DocumentFilter<SwaggerTagAttributeDocumentFilter>();
}
}
}
48 changes: 48 additions & 0 deletions src/Swashbuckle.AspNetCore.Annotations/SwaggerTagAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Swashbuckle.AspNetCore.Swagger;
using System;

namespace Swashbuckle.AspNetCore.Annotations
{
/// <summary>
/// Defines document tags and related descriptions
/// </summary>
/// <remarks>
/// Tags defined on a controller will be added to the main swagger object.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class SwaggerTagAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerTagAttribute"/> class.
/// </summary>
/// <param name="name">The name of the tag, if an empty or whitespace name is given it wont be included</param>
/// <param name="description">An optional description for the tag</param>
/// <param name="externalDocUrl">An optional external document url</param>
public SwaggerTagAttribute(string name, string description = null, string externalDocUrl = null)
{
this.Name = name;
this.Description = description;
this.Tag = new Tag { Name = this.Name, Description = this.Description };

if (externalDocUrl != null)
{
this.Tag.ExternalDocs = new ExternalDocs { Url = externalDocUrl };
}
}

/// <summary>
/// Gets the name of the Tag
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the Description description for the tag
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the Tag that is going to be added to the Document model.
/// </summary>
public Tag Tag { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Swashbuckle.AspNetCore.Annotations
{
/// <summary>
/// A document filter that creates Tags on the Document (Swagger object),
/// defined by the <see cref="SwaggerTagAttribute" /> on controllers.
/// </summary>
/// <remarks>
/// This filter does not alter the tags of the operations where the attribute is applied.
/// Neither does it ensure that you have unique tags, so if you tag two controllers with the same tag but two different descriptions the result is that multiple tags with different descriptions are added.
/// </remarks>
/// <seealso cref="Swashbuckle.AspNetCore.SwaggerGen.IDocumentFilter" />
public class SwaggerTagAttributeDocumentFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
var controllersWithTags = GetControllersAndTags(context);
EnsureDocumentTagsList(swaggerDoc);
ApplyTags(swaggerDoc, controllersWithTags);
}

private static void ApplyTags(SwaggerDocument swaggerDoc, Dictionary<string, IEnumerable<Tag>> controllersWithTags)
{
foreach (var tag in GetValidTags(controllersWithTags))
{
swaggerDoc.Tags.Add(tag);
}
}

private static Dictionary<string, IEnumerable<Tag>> GetControllersAndTags(DocumentFilterContext context)
{
return context.ApiDescriptions
.Select(apiDesc => apiDesc.ActionDescriptor)
.OfType<ControllerActionDescriptor>()
.Select(controllerDescriptor => new
{
ControllerName = controllerDescriptor.ControllerName,
Tags = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes<SwaggerTagAttribute>().Select(attr => attr.Tag)
})
.Where(controller => controller.Tags.Any())
.GroupBy(controller => controller.ControllerName)
.ToDictionary(group => group.Key, group => group.First().Tags);
}

private static void EnsureDocumentTagsList(SwaggerDocument swaggerDoc)
{
if (swaggerDoc.Tags == null)
{
swaggerDoc.Tags = new List<Tag>();
}
}

private static IEnumerable<Tag> GetValidTags(Dictionary<string, IEnumerable<Tag>> controllersWithTags)
{
return controllersWithTags
.SelectMany(controller => controller.Value)
.Where(tag => !string.IsNullOrWhiteSpace(tag.Name));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Swashbuckle.AspNetCore.Annotations.Test.Fixtures
{
[SwaggerTag("Tag1", "Description1")]
[SwaggerTag("Tag2", "Description2")]
[SwaggerTag("Tag42", "Description42")]
[SwaggerTag("", "ThisDescriptionShouldNotBeIncluded")]
internal class TaggedController
{
public void EmptyAction()
{ }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Swashbuckle.AspNetCore.Annotations.Test
{
[SwaggerOperationFilter(typeof(VendorExtensionsOperationFilter))]
[SwaggerTag("TestTag", "TestDescription")]
internal class TestController
{
public void ActionWithNoAttributes()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Annotations.Test.Fixtures;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Xunit;
using System.Linq;

namespace Swashbuckle.AspNetCore.Annotations.Test
{
public class SwaggerTagAttributeDocumentFilterTests
{
[Fact]
public void Apply_SetsTagWithDescription_UsingAttributeDescription()
{
var document = new SwaggerDocument();
var filterContext = this.FilterContextFor<TestController>();

Subject().Apply(document, filterContext);

Assert.Single(document.Tags);
Assert.Contains(document.Tags, tag => tag.Name == "TestTag" && tag.Description == "TestDescription");
}

[Fact]
public void Apply_MultipleTags_AddsMultipleDocumentTags()
{
var document = new SwaggerDocument();
var filterContext = this.FilterContextFor<TaggedController>();

Subject().Apply(document, filterContext);

Assert.Equal(3, document.Tags.Count);

// This would be so much more pretty with FluentValidation or some such (Collection Equivalence checks)
// Alternatively if Tag could implement IEquatable or there was a comparer.
Assert.Contains(document.Tags, tag => tag.Name == "Tag1" && tag.Description == "Description1");
Assert.Contains(document.Tags, tag => tag.Name == "Tag2" && tag.Description == "Description2");
Assert.Contains(document.Tags, tag => tag.Name == "Tag42" && tag.Description == "Description42");
Assert.DoesNotContain(document.Tags, tag => tag.Description == "ThisDescriptionShouldNotBeIncluded");
}

private DocumentFilterContext FilterContextFor<TController>()
{
var filterContext = new DocumentFilterContext(
null,
new[]
{
new ApiDescription
{
ActionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(TController).GetTypeInfo(),
ControllerName = typeof(TController).Name
}
},
},
null);
return filterContext;
}

private SwaggerTagAttributeDocumentFilter Subject()
{
return new SwaggerTagAttributeDocumentFilter();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace Basic.Controllers
{
[SwaggerTag("Carts", "Manipulate Carts to your heart's content", "http://www.github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md")]
public class SwaggerAnnotationsController
{
[SwaggerOperation("CreateCart")]
[SwaggerOperation("CreateCart", Tags = new string[] { "Carts", "Checkout" })]
[HttpPost("/carts")]
public Cart Create([FromBody]Cart cart)
{
Expand Down