payetools-steve
payetools-steve

Reputation: 4030

How to omit methods from Swagger documentation on WebAPI using Swashbuckle

I have a C# ASP.NET WebAPI application with API documentation being automatically generated using Swashbuckle. I want to be able to omit certain methods from the documentation but I can't seem to work out how to tell Swagger not to include them in the Swagger UI output.

I sense it is something to do with adding a model or schema filter but it isn't obvious what to do and the documentation only seems to provide examples of how to modify the output for a method, not remove it completely from the output.

Upvotes: 306

Views: 222495

Answers (16)

sommmen
sommmen

Reputation: 7658

And addendum to https://stackoverflow.com/a/76837366/4122889, here's a sample on how to only include publicly facing apis:

The nice thing is that instead of the document filter this is faster and does not leave you with extra schema's.


        services.AddSwaggerGen(c =>
        {
            CommonSwaggerGenConfig(c);

            // Only publicly accessible methods
            //c.DocumentFilter<PublicApiDocumentFilter>();

            c.DocInclusionPredicate((docName, desc) =>
            {
                // Only apply for the public api
                if (docName != "public")
                    return true;

                return desc.ActionDescriptor.EndpointMetadata.Any(x => x is AllowAnonymousAttribute);
            });

            c.SwaggerDoc("public", new()
            {
                Title = "...",
                ...
            });
            
            // Remove JWT Authentication for public Swagger document
            // No security definitions or requirements for the public document
        });

Upvotes: 0

Aliaksandr Markau
Aliaksandr Markau

Reputation: 1

For Net6 and to avoid removing all paths if you have several http methods for the same path

public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
    foreach (var apiDescription in context.ApiDescriptions)
    {
        if (!apiDescription.ActionDescriptor.FilterDescriptors.Any(d => d.Filter is ApiDocumentationAttribute))
        {
            var route = "/" + apiDescription.RelativePath?.TrimEnd('/');
            var path = swaggerDoc.Paths[route];
            if (path != null)
            {
                if (Enum.TryParse<OperationType>(apiDescription.HttpMethod, true, out var operationType))
                {
                    path.Operations.Remove(operationType);
                }
            }
        }
    }
}

Upvotes: 0

Murilo Maciel Curti
Murilo Maciel Curti

Reputation: 3131

[NonAction] Indicates that a controller method is not an action method. It belongs to the namespace Microsoft.AspNetCore.Mvc https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.nonactionattribute

In this example of implementation of IActionFilter on a Controller the Swagger generation works fine. Without NonAction Swagger throws and exception Ambiguous HTTP method for action... Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0

public class MyController
{
    
    [HttpGet]
    public async Task<int> CountAsync()
    {
        return 1;
    }
    
    [NonAction]
    public void OnActionExecuting(ActionExecutingContext context){ }

    [NonAction]
    public void OnActionExecuted(ActionExecutedContext context) { }
    
}

Upvotes: 2

MatteoSp
MatteoSp

Reputation: 3048

All the solutions proposed are based on IDocumentFilter, which isn't ideal - for me - because it gets called only once everything has been generated and you end up with removing paths but preserving schemas generated because of those paths.

With Net6.0 (may be even with some previous version, I'm not sure) you can work at an earlier stage, like this:

services.AddSwaggerGen(c =>
{
    // all the usual stuff omitted...

    c.DocInclusionPredicate((_, description) =>
    {
        var actionDescriptor = (ControllerActionDescriptor)description.ActionDescriptor;

        return actionDescriptor.ControllerTypeInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any()
               || actionDescriptor.MethodInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any();

        //or any other visibility strategy...
    });
});

This will remove unwanted paths and related schemas too.

Upvotes: 5

RHarmon
RHarmon

Reputation: 23

If you're using the new Minimal API approach, you can use .ExcludeFromDescription(); on the EndpointBuilder for the method you want to exclude from the documentation. For example, this excludes the GET method at the "/greeting" endpoint:

app.MapGet("/greeting", () => "Hello World!").ExcludeFromDescription();

Documentation is here: RouteHandlerBuilder.ExcludeFromDescription

Upvotes: 1

Jck
Jck

Reputation: 191

If you are using the minimal API you can use:

app.MapGet("/hello", () => "Hello World!").ExcludeFromDescription();

Upvotes: 14

Qu&#253; Nguyễn
Qu&#253; Nguyễn

Reputation: 31

You can create a custom filter at both Controller and Method level. So any Controller/Method with your attribute will be available in the Swagger doc. This filter also removed the duplicate HTTP verbs from your document (in this example I make it for GET/PUT/POST/PATCH only), however, you can always customize per your requirement

The attribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class PublicApi:Attribute
{

}

Document filter

public class PublicApiFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {

        var publicPaths = new List<string> {"/api"};

        var publicApiDescriptions = new List<ApiDescription>();

        var publicMethods = FilterByPublicControllers(swaggerDoc, apiExplorer, publicPaths, publicApiDescriptions);

        FilterByPublicActions(swaggerDoc, publicApiDescriptions, publicMethods);
    }

    private static Dictionary<string, List<string>> FilterByPublicControllers(SwaggerDocument swaggerDoc, IApiExplorer apiExplorer, List<string> publicPaths, List<ApiDescription> publicApiDescriptions)
    {
        var publicMethods = new Dictionary<string, List<string>>();
        foreach (var apiDescription in apiExplorer.ApiDescriptions)
        {
            var isPublicApiController = apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<PublicApi>().Any();
            var isPublicApiMethod = apiDescription.ActionDescriptor.GetCustomAttributes<PublicApi>().Any();


            if (!isPublicApiController && !isPublicApiMethod)
            {
                continue;
            }

            var relativePath = ToRelativePath(apiDescription);

            publicPaths.Add(relativePath);
            publicApiDescriptions.Add(apiDescription);

            var action = apiDescription.ActionDescriptor.ActionName;
            List<string> available = null;
            if (!publicMethods.TryGetValue(relativePath, out available))
                publicMethods[relativePath] = new List<string>();
            publicMethods[relativePath].Add(action);
        }

        swaggerDoc.paths = swaggerDoc.paths.Where(pair => publicPaths.Contains(pair.Key))
            .ToDictionary(pair => pair.Key,
                pair => pair.Value);
        return publicMethods;
    }

    private static void FilterByPublicActions(SwaggerDocument swaggerDoc, List<ApiDescription> publicApis, Dictionary<string, List<string>> publicMethods)
    {
        foreach (var api in publicApis)
        {
            var relativePath = ToRelativePath(api);
            var availableActions = publicMethods[relativePath];
            if (availableActions == null)
            {
                continue;
            }

            foreach (var path in swaggerDoc.paths.Where(pair => pair.Key.IndexOf(relativePath) > -1).ToList())
            {
                if (!availableActions.Contains("Get"))
                    path.Value.get = null;
                if (!availableActions.Contains("Post"))
                    path.Value.post = null;
                if (!availableActions.Contains("Put"))
                    path.Value.put = null;
                if (!availableActions.Contains("Patch"))
                    path.Value.patch = null;
            }
        }
    }

    private static string ToRelativePath(ApiDescription apiDescription)
    {
        return "/" + apiDescription.RelativePath.Substring(0,apiDescription.RelativePath.LastIndexOf('/'));
    }
}

And finally, register your SwaggerConfig

public class SwaggerConfig
{
    public static void Register()
    {

        var thisAssembly = typeof(SwaggerConfig).Assembly;
        GlobalConfiguration.Configuration
            .EnableSwagger(c =>
                {
                    c.SingleApiVersion("v1", "Reports");
                    c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
                    c.DocumentFilter<PublicApiFilter>();
                })
            .EnableSwaggerUi(c =>
                {

                });

    }
}

Examples

Controller

[PublicApi]
public class ProfileController : ApiController

Method

 public class UserController : ApiController
 {
    [PublicApi]
    public ResUsers Get(string sessionKey, int userId, int groupId) {
        return Get(sessionKey, userId, groupId, 0);
    }

Upvotes: 2

GavKilbride
GavKilbride

Reputation: 1569

Like @aleha I wanted to exclude by default so that I didn't accidentally expose an endpoint by accident (secure by default) but was using a newer version of the Swagger that uses OpenApiDocument.

Create a ShowInSwagger Attribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class ShowInSwaggerAttribute : Attribute
{}

Then create a Document Filter

using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System;
using System.Linq;
using TLS.Common.Attributes;

namespace TLS.Common.Filters
{
    public class ShowInSwaggerFilter : IDocumentFilter
    {
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            foreach (var contextApiDescription in context.ApiDescriptions)
            {
                var actionDescriptor = (ControllerActionDescriptor)contextApiDescription.ActionDescriptor;

                if (actionDescriptor.ControllerTypeInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any() ||
                    actionDescriptor.MethodInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any())
                {
                    continue;
                }
                else
                {
                    var key = "/" + contextApiDescription.RelativePath.TrimEnd('/');
                    var operation = (OperationType)Enum.Parse(typeof(OperationType), contextApiDescription.HttpMethod, true);

                    swaggerDoc.Paths[key].Operations.Remove(operation);

                    // drop the entire route of there are no operations left
                    if (!swaggerDoc.Paths[key].Operations.Any())
                    {
                        swaggerDoc.Paths.Remove(key);
                    }
                }
            }
        }
    }
}

then in your startup.cs or ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
     // other code

    services.AddSwaggerGen(c =>
    {
        c.DocumentFilter<ShowInSwaggerFilter>();
        // other config
    });
}

Upvotes: 9

Rowan Archer
Rowan Archer

Reputation: 143

Make a filter

public class SwaggerTagFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        foreach(var contextApiDescription in context.ApiDescriptions)
        {
            var actionDescriptor = (ControllerActionDescriptor)contextApiDescription.ActionDescriptor;
            
            if(!actionDescriptor.ControllerTypeInfo.GetCustomAttributes<SwaggerTagAttribute>().Any() && 
               !actionDescriptor.MethodInfo.GetCustomAttributes<SwaggerTagAttribute>().Any())
            {
                var key = "/" + contextApiDescription.RelativePath.TrimEnd('/');
                swaggerDoc.Paths.Remove(key);
            }
        }
    }
}

Make an attribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class SwaggerTagAttribute : Attribute
{
}

Apply in startup.cs

services.AddSwaggerGen(c => {
    c.SwaggerDoc(1, new Info { Title = "API_NAME", Version = "API_VERSION" });
    c.DocumentFilter<SwaggerTagFilter>(); // [SwaggerTag]
});

Add [SwaggerTag] attribute to methods and controllers you want to include in Swagger JSON

Upvotes: 12

Vikramraj
Vikramraj

Reputation: 101

Add one line to the SwaggerConfig

c.DocumentFilter<HideInDocsFilter>();

...

public class HideInDocsFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    { 
        var pathsToRemove = swaggerDoc.Paths
            .Where(pathItem => !pathItem.Key.Contains("api/"))
            .ToList();
    
        foreach (var item in pathsToRemove)
        {
            swaggerDoc.Paths.Remove(item.Key);
        }
    }
}

Upvotes: 5

S P
S P

Reputation: 726

May help somebody but during development (debugging) we like to expose whole Controllers and/or Actions and then hide these during production (release build)

#if DEBUG
    [ApiExplorerSettings(IgnoreApi = false)]
#else
    [ApiExplorerSettings(IgnoreApi = true)]
#endif  

Upvotes: 60

Denis Biondic
Denis Biondic

Reputation: 8201

I would prefer to remove the dictionary entries for path items completely:

var pathsToRemove = swaggerDoc.Paths
                .Where(pathItem => !pathItem.Key.Contains("api/"))
                .ToList();

foreach (var item in pathsToRemove)
{
    swaggerDoc.Paths.Remove(item.Key);
}

With this approach, you would not get "empty" items in the generated swagger.json definition.

Upvotes: 13

aleha_84
aleha_84

Reputation: 8539

Based on @spottedmahns answer. My task was vice versa. Show only those that are allowed.

Frameworks: .NetCore 2.1; Swagger: 3.0.0

Added attribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class ShowInSwaggerAttribute : Attribute
{
}

And implement custom IDocumentFilter

public class ShowInSwaggerFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {

        foreach (var contextApiDescription in context.ApiDescriptions)
        {
            var actionDescriptor = (ControllerActionDescriptor) contextApiDescription.ActionDescriptor;

            if (actionDescriptor.ControllerTypeInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any() ||
                actionDescriptor.MethodInfo.GetCustomAttributes<ShowInSwaggerAttribute>().Any())
            {
                continue;
            }
            else
            {
                var key = "/" + contextApiDescription.RelativePath.TrimEnd('/');
                var pathItem = swaggerDoc.Paths[key];
                if(pathItem == null)
                    continue;

                switch (contextApiDescription.HttpMethod.ToUpper())
                {
                    case "GET":
                        pathItem.Get = null;
                        break;
                    case "POST":
                        pathItem.Post = null;
                        break;
                    case "PUT":
                        pathItem.Put = null;
                        break;
                    case "DELETE":
                        pathItem.Delete = null;
                        break;
                }

                if (pathItem.Get == null  // ignore other methods
                    && pathItem.Post == null 
                    && pathItem.Put == null 
                    && pathItem.Delete == null)
                    swaggerDoc.Paths.Remove(key);
            }
        }
    }
}

ConfigureServices code:

public void ConfigureServices(IServiceCollection services)
{
     // other code

    services.AddSwaggerGen(c =>
    {
        // other configurations
        c.DocumentFilter<ShowInSwaggerFilter>();
    });
}

Upvotes: 4

Paulo Pozeti
Paulo Pozeti

Reputation: 389

Someone posted the solution on github so I'm going to paste it here. All credits goes to him. https://github.com/domaindrivendev/Swashbuckle/issues/153#issuecomment-213342771

Create first an Attribute class

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class HideInDocsAttribute : Attribute
{
}

Then create a Document Filter class

public class HideInDocsFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {
        foreach (var apiDescription in apiExplorer.ApiDescriptions)
        {
            if (!apiDescription.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<HideInDocsAttribute>().Any() && !apiDescription.ActionDescriptor.GetCustomAttributes<HideInDocsAttribute>().Any()) continue;
            var route = "/" + apiDescription.Route.RouteTemplate.TrimEnd('/');
            swaggerDoc.paths.Remove(route);
        }
    }
}

Then in Swagger Config class, add that document filter

public class SwaggerConfig
{
    public static void Register(HttpConfiguration config)
    {
        var thisAssembly = typeof(SwaggerConfig).Assembly;

        config
             .EnableSwagger(c =>
                {
                    ...                       
                    c.DocumentFilter<HideInDocsFilter>();
                    ...
                })
            .EnableSwaggerUi(c =>
                {
                    ...
                });
    }
}

Last step is to add [HideInDocsAttribute] attribute on the Controller or Method you don't want Swashbuckle to generate documentation.

Upvotes: 30

mikesigs
mikesigs

Reputation: 11420

You can add the following attribute to Controllers and Actions to exclude them from the generated documentation: [ApiExplorerSettings(IgnoreApi = true)]

Upvotes: 747

Dave Transom
Dave Transom

Reputation: 4185

You can remove "operations" from the swagger document after it's generated with a document filter - just set the verb to null (though, there may be other ways to do it as well)

The following sample allows only GET verbs - and is taken from this issue.

class RemoveVerbsFilter : IDocumentFilter
{
    public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {
        foreach (PathItem path in swaggerDoc.paths.Values)
        {
            path.delete = null;
            //path.get = null; // leaving GET in
            path.head = null;
            path.options = null;
            path.patch = null;
            path.post = null;
            path.put = null;
        }
    }
}

and in your swagger config:

...EnableSwagger(conf => 
{
    // ...

    conf.DocumentFilter<RemoveVerbsFilter>();
});

Upvotes: 16

Related Questions