Search code examples
c#asp.net-coreswaggerswagger-ui

Swagger UI Not Filtering Routes Based on Query Parameter in ASP.NET Core


I’m working on an ASP.NET Core Web API project and I’m trying to filter the routes displayed in Swagger UI based on a consumer key provided as a query parameter. I’ve implemented a custom IDocumentFilter that should remove all paths from the Swagger document if the consumer key is not found or is null. However, when I navigate to http://localhost:7249/swagger/index.html?consumer=hasad, it’s not filtering the routes as expected.

Here’s the relevant part of my code:

// KeyBasedDocumentFilter.cs

namespace HFCDDM.Api.Filters
{
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc.Controllers;
    using Microsoft.Extensions.Logging;
    using Microsoft.OpenApi.Models;
    using Swashbuckle.AspNetCore.SwaggerGen;
    using System.Linq;
    using System.Reflection;

    public class KeyBasedDocumentFilter : IDocumentFilter
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<KeyBasedDocumentFilter> _logger;

        public KeyBasedDocumentFilter(IHttpContextAccessor httpContextAccessor, ILogger<KeyBasedDocumentFilter> logger)
        {
            _httpContextAccessor = httpContextAccessor;
            _logger = logger;
        }

        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            _logger.LogInformation("KeyBasedDocumentFilter is being executed.");

            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null || !httpContext.Request.Query.TryGetValue("consumer", out var consumerKeys) || consumerKeys.Count == 0)
            {
                _logger.LogWarning("Consumer key not found in query parameter or is null. No routes will be shown.");
                swaggerDoc.Paths.Clear(); // Clear all paths if no valid consumer key is found
                return;
            }

            var consumerKey = consumerKeys.First();
            _logger.LogInformation($"Consumer Key: {consumerKey}");

            foreach (var path in swaggerDoc.Paths.ToList())
            {
                var pathItem = path.Value;
                var controllerAttributes = context.ApiDescriptions
                    .Select(apiDesc => apiDesc.ActionDescriptor.EndpointMetadata)
                    .OfType<ControllerActionDescriptor>()
                    .SelectMany(descriptor => descriptor.ControllerTypeInfo.GetCustomAttributes<KeyAuthorizeAttribute>())
                    .ToList();

                // If no controller has the KeyAuthorizeAttribute or the consumer key doesn't match, remove the path
                if (!controllerAttributes.Any() || !controllerAttributes.Any(attr => attr.Key == consumerKey))
                {
                    swaggerDoc.Paths.Remove(path.Key);
                }
            }
        }
    }

}

// Program.cs

builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "HFCDDM.Api", Version = "v1" });
    c.DocumentFilter<KeyBasedDocumentFilter>();
});
builder.Services.AddScoped<IDocumentFilter, KeyBasedDocumentFilter>();

// Middleware configuration

app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/swagger/index.html"))
    {
        var consumerKey = context.Request.Query["consumer"].FirstOrDefault();
        if (!string.IsNullOrEmpty(consumerKey))
        {
            // Pass the consumer key to the document filter
            context.Items["ConsumerKey"] = consumerKey;
        }
    }

    await next();
});

// AccountController.cs

[Route("api/[controller]")]
[ApiController] 
[Authorize]
[KeyAuthorize("hasad")]
public class AccountController : ControllerBase
{
    // ... (controller actions)
}

// KeyAuthorizeAttribute.cs

namespace HFCDDM.Api
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class KeyAuthorizeAttribute : Attribute
    {
        public string Key { get; }

        public KeyAuthorizeAttribute(string key)
        {
            Key = key;
        }
    }
}

The KeyBasedDocumentFilter should clear all paths if the consumer key is not present or is null, but it seems like the consumer key is not being recognized correctly. The KeyAuthorize attribute is applied to the AccountController, and I expect only this controller’s routes to be shown when the correct key is provided.

What I Expect: When I access the Swagger UI without a consumer key or with a null key, I expect no routes to be shown. When I provide the correct consumer key (hasad) in the query parameter, I expect only the routes from AccountController to be visible. However, the logs indicate that the consumer key is not being found:

HFCDDM.Api.Filters.KeyBasedDocumentFilter: Warning: Consumer key not found in query parameter.


Solution

  • Here is a whole working demo you could follow:

    public class KeyBasedDocumentFilter : IDocumentFilter
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<KeyBasedDocumentFilter> _logger;
    
        public KeyBasedDocumentFilter(IHttpContextAccessor httpContextAccessor, ILogger<KeyBasedDocumentFilter> logger)
        {
            _httpContextAccessor = httpContextAccessor;
            _logger = logger;
        }
    
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            _logger.LogInformation("KeyBasedDocumentFilter is being executed.");
    
            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null || !httpContext.Request.Query.TryGetValue("consumer", out var consumerKeys) || consumerKeys.Count == 0)
            {
                _logger.LogWarning("Consumer key not found in query parameter or is null. No routes will be shown.");
                swaggerDoc.Paths.Clear(); // Clear all paths if no valid consumer key is found
                return;
            }
    
            var consumerKey = consumerKeys.First();
            _logger.LogInformation($"Consumer Key: {consumerKey}");
            var controllerTypes = Assembly.GetExecutingAssembly().GetTypes().Where(type => typeof(ControllerBase).IsAssignableFrom(type));
            // Check each controller type for KeyAuthorizeAttribute
            foreach (var controllerType in controllerTypes)
            {
                var hasKeyAuthorizeAttribute = controllerType.GetCustomAttributes<KeyAuthorizeAttribute>().Any(attr => attr.Key == consumerKey);
    
                if (!hasKeyAuthorizeAttribute)
                {
                    var controllerTypeName = controllerType.Name;
                    var controllerNameWithoutSuffix = controllerTypeName.EndsWith("Controller") ? controllerTypeName.Substring(0, controllerTypeName.Length - "Controller".Length) : controllerTypeName;
                    // Remove paths associated with this controller
                    var pathsToRemove = swaggerDoc.Paths.Keys.Where(path => path.Contains($"/{controllerNameWithoutSuffix}")).ToList();
                    foreach (var pathToRemove in pathsToRemove)
                    {
                        swaggerDoc.Paths.Remove(pathToRemove);
                    }
                }
            }
            
        }
    }
    

    And no need use the Middleware configuration like what you did.