Search code examples
asp.net-coreswaggeropenapiswashbuckleswagger-codegen

Swashbuckle ignores route parameters types when they are only declared in the route but not as input parameters in the action method


I have a base controller with some predefined route template:

[Authorize]
[ApiController]
[Produces(MediaTypeNames.Application.Json)]
public abstract class TenantControllerBase : ControllerBase
{
    public const string DefaultRoute = "api/s/{serviceId:int}/[controller]";

    private readonly ITenantContext _tenantContext; // Extracts the value of serviceId in a middelware

    protected int ServiceId => _tenantContext.ServiceId;

    protected TenantControllerBase(ITenantContext tenantContext)
    {
        _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
    }
}

The idea is to be able to retrieve serviceId using a middleware and make it available to all controllers inheriting from TenantControllerBase without having to each time explicitly mention it in the action as input parameter:

[Route(DefaultRoute)]
public class SomeTenantController : TenantControllerBase
{
    public SomeTenantController(ITenantContext tenantContext) : base(tenantContext) { }

    [HttpGet("{type}")]
    public async Task<IActionResult> Get(string type, CancellationToken cancellationToken)
    {
        var serviceId = this.ServiceId;
        
        // ...

        return Ok();
    }
}

The problem with this is that the Swagger specification sets the type of serviceId as string and not as integer as it is specified in the route template:

  "/api/s/{serviceId}/SomeTenant/{type}": {
  "get": {
    "tags": [
      "SomeTenant"
    ],
    "parameters": [
      {
        "name": "type",
        "in": "path",
        "required": true,
        "style": "simple",
        "schema": {
          "type": "string"
        }
      },
      {
        "name": "serviceId",
        "in": "path",
        "required": true,
        "style": "simple",
        "schema": {
          "type": "string"
        }
      }
    ],
    "responses": {
      "200": {
        "description": "Success"
      }
    }
  }
},

As far as I tried the only way to make Swagger understands that serviceId is an integer is by explicitly adding it to the action method signature:

    [HttpGet("{type}")]
    public async Task<IActionResult> Get(
        string type,
        int serviceId, 
        CancellationToken cancellationToken)
    {
        // serviceId is the same as the base field this.ServiceId;
        
        // ...

        return Ok();
    }

Is there any way to force swagger to set the value type according to the one specified in the route template api/s/{serviceId:int}/[controller] without having to explicitly add it in the action method signature?


Solution

  • You can patch this up with a simple operation filter to update service id parameter type for all of your endpoints:

    public class ServiceIdPathFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var serviceIdParameter = operation.Parameters
                .FirstOrDefault(x => x.Name == "serviceId" && x.In == ParameterLocation.Path);
            if (serviceIdParameter != null)
            {
                serviceIdParameter.Schema.Type = "integer";
            }
        }
    }
    

    Registration in startup:

    services.AddSwaggerGen(opts => opts.OperationFilter<ServiceIdPathFilter>());
    

    No longer valid for string values:

    enter image description here