ApiEndpoints: Binding parameters from multiple locations not working in .NET 6?

  • NuGet Package Version: 3.1.0
  • .NET SDK Version: 6.0-RC-1

I am trying to bind parameters from multiple locations as specified in the documentation, but it does not seem to work.

Endpoint:

[HttpPut("api/projects/{projectId}")]
public override async Task<ActionResult<UpdateProjectResponse>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken = default)
{

}

Request:

public class UpdateProjectRequest
{
    [FromRoute(Name = "projectId")] public Guid ProjectId { get; set; } = default!;

    [FromBody] public string Code { get; set; } = default!;

    [FromBody] public string Description { get; set; } = default!;

    [FromBody] public string Number { get; set; } = default!;
}

This is the response I get:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-8204c6f7087a1e60fb4c1ab5ac75c778-de42a5cad4fe62d6-00",
  "errors": {
    "$": [
      "The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1.",
      "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
    ],
    "Code": [
      "The Code field is required.",
      "'Code' must not be empty."
    ],
    "Number": [
      "The Number field is required.",
      "'Number' must not be empty."
    ],
    "Description": [
      "The Description field is required.",
      "'Description' must not be empty."
    ]
  }
}

I am also using FluentValidation, but that should not matter because FluentValidation validates after model binding.

Could this be because something has changed in .NET 6 or am I just missing something?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 18 (7 by maintainers)

Most upvoted comments

What I have done myself as a workaround is creating a BaseAsyncEndpoint myself with multiple WithResponse classes. One with only one parameter, one with two parameters etc.

I have done it like this: https://gist.github.com/luuk777w/5f8d1a7a6f178da4884e87ca7c0b9e38 It works really well, and as a bonus it plays nice with Swagger. However, the downside of this approach is that you will limit the number of parameters by how many classes you define.

@ardalis I have just tested it and the issue remains. I have created a reproduction repo: https://github.com/luuk777w/ApiEndpointsIssueRepro

I am not having exactly the same result as described above, I do however not get the Id from the route (though it is displayed in swagger ui as a required route parameter), but the body also includes the id and this is what is being bound to the command object.

Not sure whether this is a swagger issue or an ApiEndpoint issue.

After finding a previous problem I managed to work around this issue, maybe it’ll help solve the underlying issue.

must include FromRoute in handler:

HandleAsync([FromRoute]FinishRequest request,

then the request can be split into two parts:

    public class FinishRequest
    {
        public const string Route = "/shift/{callsign}/end";

        [FromRoute(Name = "callsign")]
        public string CallSign { get; set; } = null!;

        [FromBody]
        public FinishRequestBody Body { get; set; } = null!;

        public class FinishRequestBody
        {
            /// <summary>
            /// The date and time when the shift was finished
            /// </summary>
            public DateTimeOffset EndTime { get; set; }
        }
    }

The only downside is that referencing the request will need an additional property: request.Body.EndTime . I don’t see this as a big deal as it further defines where the data is coming from.

As a bonus, Swagger displays and passes the request as expected too, and correctly defined the Body component.