domaindrivendev / Swashbuckle.WebApi

Seamlessly adds a swagger to WebApi projects!
BSD 3-Clause "New" or "Revised" License
3.07k stars 678 forks source link

Removing unused defintions #732

Closed pmcevoy closed 7 years ago

pmcevoy commented 8 years ago

I recently updated our ApiController action methods to return a generic type that inherits from HttpResponseMessage (we call it TypedHttpResponseMessage<T>) and to use the [SwaggerResponse] attribute to document the return types as the <T>. However when I pasted the generated swagger into http://editor.swagger.io it warned me about unused defintions relating to the TypedHttpResponseMessage. (Seems that this does not happen if I use a raw HttpResponseMessage)

So I wrote an IDocumentFilter that will remove unused defintions and thought I post it here in case someone else gets the same issue (and if anyone cares to code-review I won't mind either!)

YMMV - we don't use all the features of a swagger file, so this filter may need to be updated if you use other schemas

    public class RemoveUnusedDefinitions : IDocumentFilter
    {
        private readonly Dictionary<string, int> _countByDefinitionRef = new Dictionary<string, int>();

        public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
        {
            bool aDefinitionWasRemoved;

            do
            {
                aDefinitionWasRemoved = RemoveUnused(swaggerDoc, schemaRegistry);
            } while (aDefinitionWasRemoved);
        }

        private bool RemoveUnused(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry)
        {
            foreach (var refWithSchema in schemaRegistry.Definitions)
                _countByDefinitionRef["#/definitions/" + refWithSchema.Key] = 0;

            foreach (var pathItem in swaggerDoc.paths.Values)
            {
                CountParameterRefs(pathItem.parameters);
                CountOperationRefs(pathItem.delete);
                CountOperationRefs(pathItem.get);
                CountOperationRefs(pathItem.head);
                CountOperationRefs(pathItem.options);
                CountOperationRefs(pathItem.patch);
                CountOperationRefs(pathItem.post);
                CountOperationRefs(pathItem.put);
            }

            CountDefinitionsRefs(swaggerDoc.definitions);

            var aDefinitionWasRemoved = false;

            foreach (var countByRef in _countByDefinitionRef)
                if (countByRef.Value == 0)
                {
                    var definitionKey = countByRef.Key.Replace("#/definitions/", "");
                    if (swaggerDoc.definitions.Remove(definitionKey))
                        aDefinitionWasRemoved = true;
                }

            return aDefinitionWasRemoved;
        }

        private void CountDefinitionsRefs(IDictionary<string, Schema> definitions)
        {
            foreach (var definition in definitions.Values)
                CountSchemaRefs(definition);
        }

        private void CountOperationRefs(Operation operation)
        {
            if (operation == null)
                return;

            CountParameterRefs(operation.parameters);
            CountResponseRefs(operation.responses);
        }

        private void CountResponseRefs(IDictionary<string, Response> responsesByHttpStatus)
        {
            if (responsesByHttpStatus == null)
                return;

            foreach (var response in responsesByHttpStatus.Values)
                CountSchemaRefs(response.schema);
        }

        private void CountParameterRefs(IList<Parameter> parameters)
        {
            if (parameters == null)
                return;

            foreach (var param in parameters)
                CountSchemaRefs(param.schema);
        }

        private void CountSchemaRefs(Schema schema)
        {
            if (schema == null)
                return;

            if (!string.IsNullOrEmpty(schema.@ref))
                _countByDefinitionRef[schema.@ref]++;

            if (schema.items != null && !string.IsNullOrEmpty(schema.items.@ref))
                _countByDefinitionRef[schema.items.@ref]++;

            if (schema.properties != null)
                //Recurse into child properties
                foreach (var s in schema.properties.Values)
                    CountSchemaRefs(s);
        }
    }
domaindrivendev commented 8 years ago

When you have actions that return a type that doesn't quite correspond to the actual API contract (e.g. HttpResponseMessage, IHttpActionResult or in your case HttpResponseMessage<T>, WebAPI provides the [ResponseType] attribute to inform it's metadata layer (ApiExplorer) of the "acctual" response type. If you use this for the success case and just use [SwaggerResponse] for the error cases, you won't get the redundant definition:

[ResponseType(typeof(Product))]
[SwaggerResponse(400, "Validation Error", typeof(HttpError))]
public HttpResponseMessage<Product> Create(Product product)
{
    ....

Right now, this would be the recommended approach for your case. It does mean you have to mix and match framework attributes with Swashbuckle attributes to cover success and error response codes which isn't ideal. However, in ASP.NET Core (next-gen), the framework introduces a new attribute [ProducesResponseType] for covering the error cases. So, in the next major version of SB (targeting ASP.NET Core) the SwaggerResponse attributes will be removed in favor of the built-in ones.

pmcevoy commented 8 years ago

Interesting - we'll give that a shot. If I can remove that custom filter, I'll be happy.

domaindrivendev commented 7 years ago

There is now a separate project specifically for ASP.NET Core and this allows you to describe all response codes purely with attributes that are native to that framework:

https://github.com/domaindrivendev/Swashbuckle.AspNetCore

Of course this is only available if you've moved your application to ASP.NET Core. If this isn't on the cards, then you're stuck with the workarounds described above

radoslav-h-todorov commented 5 years ago

It works for me, @pmcevoy. Thank you!

niemyjski commented 2 years ago

@pmcevoy Thanks for posting this. I was wondering if you have similar solution for ASP.NET Core?

bkoelman commented 7 months ago

Simpler solution (works with ASP.NET Core):

using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Services;
using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// Removes unreferenced component schemas from the OpenAPI document.
/// </summary>
internal sealed class UnusedComponentSchemaCleaner : IDocumentFilter
{
    public void Apply(OpenApiDocument document, DocumentFilterContext context)
    {
        var visitor = new OpenApiReferenceVisitor();
        var walker = new OpenApiWalker(visitor);
        walker.Walk(document);

        HashSet<string> unusedSchemaNames = [];

        foreach (string schemaId in document.Components.Schemas
            .Select(schema => schema.Key)
            .Where(schemaId => !visitor.UsedSchemaNames.Contains(schemaId)))
        {
            unusedSchemaNames.Add(schemaId);
        }

        foreach (string schemaId in unusedSchemaNames)
        {
            document.Components.Schemas.Remove(schemaId);
        }
    }

    private sealed class OpenApiReferenceVisitor : OpenApiVisitorBase
    {
        private const string ComponentSchemaPrefix = "#/components/schemas/";

        public HashSet<string> UsedSchemaNames { get; } = [];

        public override void Visit(IOpenApiReferenceable referenceable)
        {
            UsedSchemaNames.Add(referenceable.Reference.Id);
        }

        public override void Visit(OpenApiSchema schema)
        {
            if (schema.Discriminator != null)
            {
                foreach (string discriminatorValue in schema.Discriminator.Mapping.Values)
                {
                    if (discriminatorValue.StartsWith(ComponentSchemaPrefix, StringComparison.Ordinal))
                    {
                        string schemaId = discriminatorValue[ComponentSchemaPrefix.Length..];
                        UsedSchemaNames.Add(schemaId);
                    }
                }
            }
        }
    }
}