version:4.0.1
[HttpPost]
[Route("BatchUpdate")]
[ProducesResponseType(typeof(ResultJsonNoDataInfo), 200)]
public async Task

This repros all the way back from version 1.1.0. We see this with a "FromRoute" parameter also


I think this PR attempted to fix the issue, https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/553 but the behaviour appears to have regressed
Apologies for spam, here is a snip which works for us for our route parameters. You can make it more robust/modify it to make it honour the [FromRoute] [FromForm] etc. attributes if needed
``` ///
/// Fixes an issue which causes parameters with custom model binders to be treated as query parameters by swashbuckle.
///
public class ModelBindingRouteParameterFilter : IOperationFilter
{
///
/// When a custom model binder is applied to an input parameter, swashbuckle will mark that parameter as input type "query",
/// even if the input is a route param (either implicitly or marked with "FromRoute").
/// Here we override the parameter input type, and re-add required, only when the parameter name matches a route parameter.
///
/// The API operation.
/// Operation filter context provided.
public void Apply(Operation operation, OperationFilterContext context)
{
var parameterDescriptions = context.ApiDescription.ParameterDescriptions;
for (int i = 0; i < parameterDescriptions.Count; i++)
{
var parameterDescription = parameterDescriptions[i];
if (parameterDescription.Source == BindingSource.Custom && context.ApiDescription.RelativePath?.Contains('{' + parameterDescription.Name + '}') == true)
{
operation.Parameters[i].In = "path";
operation.Parameters[i].Required = true;
}
}
}
}
@yuzd this is still a bug, just wondering why you closed the issue? @domaindrivendev propose to reopen this.
Hello,
I'd like to second this - it seems to be an issue in 5.0 when using a custom model binder.
For parameters directly on a method, that are decorated with a custom modelbinder, they are set as query in Swagger, and not required. Decorating them with [FromRoute] and [Required] fixes it.
Same thing goes when passing a model decorated with [FromForm], then any parameter in the model that have a custom modelbinder, will come out as query in swagger, instead of RequestBody. It doesn't seem like there is any easy fix for this, besides writing your own OperationFilter that removes them from Parameters and adds them to RequestBody.
Swashbuckle is built on top of ApiExplorer the API metadata provider that ships with ASP.NET Core. It exposes an ApiParameterDescription object for each parameter and that is used by Swashbuckle to generate a corresponding OpenApiParameter object. In determining the parameter location (path, query, header etc.), Swashbuckle looks at the ApiParameterDescription.Source property and maps the value accordingly:
private static readonly Dictionary<BindingSource, ParameterLocation> ParameterLocationMap = new Dictionary<BindingSource, ParameterLocation>
{
{ BindingSource.Query, ParameterLocation.Query },
{ BindingSource.Header, ParameterLocation.Header },
{ BindingSource.Path, ParameterLocation.Path }
...
Unfortunately, the ApiExplorer sets the ApiParameterDescription.Source = BindingSource.Custom whenever a custom binder is applied regardless of whether or not the underlying parameter is also decorated with an explicit binding attribute ([FromForm], [FromQuery] etc.).
So, with the current approach, there's no good way for Swashbuckle to determine the intended parameter location unless it bypasses the ApiExplorer abstractions and inspects the attributes itself directly - and this is something I don't want to do from a design/simplicity standpoint.
In an ideal world, there would be a standard approach for expressing the intended binding source, either by attributes or additional meteadata on the IModelBinder interface, that is then projected by ApiExplorer onto the ApiParameterDescription object. Perhaps you could create an upstream issue in the ASP.NET Core repo for this?
In lieue of that, this needs to be solved with custom filters. With that said though, you could possibly simplify the filter implementation by using an IParameterFilter instead of an IOperationFilter
Hello,
I'd like to second this - it seems to be an issue in 5.0 when using a custom model binder.For parameters directly on a method, that are decorated with a custom modelbinder, they are set as
queryin Swagger, and not required. Decorating them with[FromRoute]and[Required]fixes it.Same thing goes when passing a model decorated with
[FromForm], then any parameter in the model that have a custom modelbinder, will come out asqueryin swagger, instead ofRequestBody. It doesn't seem like there is any easy fix for this, besides writing your own OperationFilter that removes them fromParametersand adds them toRequestBody.
Thank you for your analysis and suggestion! I did exactly that, the snippet is below.
My usecase was a custom BindingSource [FromBodyAndRoute] that combines Path parameters and Body into a single Model. I made it to return true for CanAcceptDataFrom(BindingSource.Body), so the filter below is generic for any other BindingSource that accepts body data.
```c#
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
// Converts any custom BindingSource that accepts Body into RequestBody parameters in Swagger
// See: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1002#issuecomment-760002223
public class CustomFromBodyOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiBodyParameter =
context.ApiDescription.ParameterDescriptions.FirstOrDefault(p =>
p.Source.CanAcceptDataFrom(BindingSource.Body));
if (apiBodyParameter == null) return;
var swaggerQueryParameter = operation.Parameters
.FirstOrDefault(p => p.Name == apiBodyParameter.Name && p.In == ParameterLocation.Query);
if (swaggerQueryParameter == null) return;
operation.Parameters.Remove(swaggerQueryParameter);
operation.RequestBody = new OpenApiRequestBody {
Content = {["application/json"] = new OpenApiMediaType {Schema = swaggerQueryParameter.Schema}}
};
}
Apply:
```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c => {
c.OperationFilter<CustomFromBodyOperationFilter>();
// ...
});
}
Most helpful comment
Apologies for spam, here is a snip which works for us for our route parameters. You can make it more robust/modify it to make it honour the [FromRoute] [FromForm] etc. attributes if needed
``` ///
/// Fixes an issue which causes parameters with custom model binders to be treated as query parameters by swashbuckle.
///
public class ModelBindingRouteParameterFilter : IOperationFilter
{
///
/// When a custom model binder is applied to an input parameter, swashbuckle will mark that parameter as input type "query",
/// even if the input is a route param (either implicitly or marked with "FromRoute").
/// Here we override the parameter input type, and re-add required, only when the parameter name matches a route parameter.
///
/// The API operation.
/// Operation filter context provided.
public void Apply(Operation operation, OperationFilterContext context)
{
var parameterDescriptions = context.ApiDescription.ParameterDescriptions;
for (int i = 0; i < parameterDescriptions.Count; i++)
{
var parameterDescription = parameterDescriptions[i];
if (parameterDescription.Source == BindingSource.Custom && context.ApiDescription.RelativePath?.Contains('{' + parameterDescription.Name + '}') == true)
{
operation.Parameters[i].In = "path";
operation.Parameters[i].Required = true;
}
}
}
}