Hide/show controllers in SwaggerUI (configurable)

Viewed 1075

We would like SwaggerUI, which is being generated by Swashbuckle, to show all controllers and methods when debugging as well as on our test environment, but hide some on integration and production environments. Note that the hidden controllers/methods will be functional in all scenarios but won't be documented in SwaggerUI.

To do this I've applied the principal described here.

Which results in following code:

Attribute definition:

using System;
using Microsoft.AspNetCore.Mvc;

namespace Test
{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public class SwaggerUIVisibilityAttribute : ApiExplorerSettingsAttribute
    {
        public SwaggerUIVisibilityAttribute(SwaggerUIVisibility visibility)
        {
            Visibility = visibility;
        }

        public SwaggerUIVisibility Visibility { get; }
    }

    public enum SwaggerUIVisibility
    {
        Debug,
        Internal,
        Public
    }
}

Filter (work in progress):

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace Test
{
    public class SwaggerUIVisibilityFilter : IDocumentFilter
    {
        #region Private Fields

        private readonly SwaggerUIVisibility _swaggerUIVisibility;

        #endregion

        #region Constructors

        public SwaggerUIVisibilityFilter(SwaggerUIVisibility swaggerUIVisibility)
        {
            _swaggerUIVisibility = swaggerUIVisibility;
        }

        #endregion

        #region Public Methods

        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var controllerActionDescriptor = apiDescription.ActionDescriptor as ControllerActionDescriptor;

                var controllerSwaggerUIVisibilityAttribute = controllerActionDescriptor?.ControllerTypeInfo
                                                                                       .GetCustomAttributes<SwaggerUIVisibilityAttribute>()
                                                                                       .SingleOrDefault();

                var methodSwaggerUIVisibilityAttribute = controllerActionDescriptor?.MethodInfo
                                                                                   .GetCustomAttributes<SwaggerUIVisibilityAttribute>()
                                                                                   .SingleOrDefault();

                var mustHideController = MustHide(controllerSwaggerUIVisibilityAttribute?.Visibility);

                if (mustHideController && controllerSwaggerUIVisibilityAttribute != null)
                {
                    // TODO: Hide controller. How?
                    //controllerSwaggerUIVisibilityAttribute.IgnoreApi = true;
                }

                var mustHideMethod = MustHide(methodSwaggerUIVisibilityAttribute?.Visibility);

                if (mustHideMethod)
                {
                    var route = "/" + apiDescription.RelativePath.TrimEnd('/');
                    swaggerDoc.Paths.Remove(route);
                }
            }
        }

        #endregion

        #region Private Methods

        private bool MustHide(SwaggerUIVisibility? visibility)
        {
            return visibility switch
            {
                SwaggerUIVisibility.Debug => _swaggerUIVisibility != SwaggerUIVisibility.Debug,
                SwaggerUIVisibility.Internal => _swaggerUIVisibility != SwaggerUIVisibility.Debug
                                             && _swaggerUIVisibility != SwaggerUIVisibility.Internal,
                SwaggerUIVisibility.Public => _swaggerUIVisibility != SwaggerUIVisibility.Debug
                                           && _swaggerUIVisibility != SwaggerUIVisibility.Internal
                                           && _swaggerUIVisibility != SwaggerUIVisibility.Public,
                _ => false
            };
        }

        #endregion
    }
}

Registration in Startup:

services.AddSwaggerGen(options =>
   {
      [...]
      options.DocumentFilter<SwaggerUIVisibilityFilter>(SwaggerUIVisibility.Internal);
   });

The SwaggerUIVisibilityAttribute attribute can be set for classes as well as methods. Everything works fine to hide methods.

Issue: I couldn't find a way to remove the controller entirely. I think my question is: How can I remove a controller to NOT show up in SwaggerUI without having to set the [ApiExplorerSettings(IgnoreApi = true)] attribute, which has to be set at compile time?

What I've tried:

  • Let SwaggerUIVisibilityAttribute extend ApiExplorerSettingsAttribute and set IgnoreApi = true in the filter when the condition to hide the controller is met. Result: It is still being displayed (I guess I'm too late in the pipeline)
  • In SwaggerUIVisibilityFilter.Apply, remove the controller from DocumentFilterContext.ApiDescriptions. Result: Not possible as this property is a get only IEnumerable
2 Answers

I found the solution in this blog post. Thanks @juunas for this

To solve my issue I've kept the code from the initial question to hide controller methods. To hide controllers I've implemented a IActionModelConvention:

namespace Test
{
    /// <summary>
    /// Shows/hides controllers in SwaggerUI. Specify the <see cref="SwaggerUIVisibility"/> in the constructor, when instantiating this class; only methods having a equal or higher visibility will be displayed.<br/>
    /// Visibility order: <see cref="SwaggerUIVisibility.Public"/> > <see cref="SwaggerUIVisibility.Internal"/> > <see cref="SwaggerUIVisibility.Debug"/>
    /// </summary>
    /// <seealso cref="Microsoft.AspNetCore.Mvc.ApplicationModels.IActionModelConvention" />
    public class SwaggerUIControllerVisibilityConvention : IActionModelConvention
    {
        #region Private Fields

        private readonly SwaggerUIVisibility _swaggerUIVisibility;

        #endregion

        #region Constructors

        public SwaggerUIControllerVisibilityConvention(SwaggerUIVisibility swaggerUIVisibility)
        {
            _swaggerUIVisibility = swaggerUIVisibility;
        }

        #endregion

        #region Public Methods

        public void Apply(ActionModel action)
        {
            var controllerSwaggerUIVisibilityAttribute = action.Controller
                                                               .Attributes
                                                               .OfType<SwaggerUIVisibilityAttribute>()
                                                               .SingleOrDefault();

            action.ApiExplorer.IsVisible = _swaggerUIVisibility.GetIsVisible(controllerSwaggerUIVisibilityAttribute?.Visibility);
        }

        #endregion
    }
}

And registered it in Startup as well

services.AddControllers(options =>
   {                                        
      [...]
      options.Conventions.Add(new SwaggerUIControllerVisibilityConvention(apiConfiguration.SwaggerUIVisibility));
   })

It's simple. Put the following attribute above any action or controller you want to hide:

[ApiExplorerSettings(IgnoreApi = true)]
Related