ardalis / ApiEndpoints

A project for supporting API Endpoints in ASP.NET Core web applications.
MIT License
3.13k stars 227 forks source link

Use Route and Body Parameters in an Endpoint #124

Closed ardalis closed 3 years ago

ardalis commented 3 years ago

For a PUT request that is making an update to a resource, how would an API endpoint allow the ID of the resource to be specified on the ROUTE while the new state of the resource is specified in the request BODY?

Some example code that is not working:

public class Put : Endpoint.WithRequest<InvestmentTypePutRequest>.WithResponse<InvestmentTypePutResponse>
{
    [HttpPut("/investment-types/{Id:Guid}")]
    public override async Task<ActionResult<InvestmentTypePutResponse>> HandleAsync(InvestmentTypePutRequest request, CancellationToken cancellationToken = default)
     {
           // request.Id is null here
      }
}

public class InvestmentTypePutRequest
{
  [FromRoute] public Guid Id { get; set; }
  [FromBody] public byte[] Version { get; set; } = null!;
  [FromBody] public string Name { get; set; } = null!;
  [FromBody] public string? Description { get; set; }
}
maxkoshevoi commented 3 years ago

Try

[HttpPut("/investment-types/{Id}")]
public override async Task<ActionResult<InvestmentTypePutResponse>> HandleAsync(Guid id, InvestmentTypePutRequest request, CancellationToken cancellationToken = default) {}

public class InvestmentTypePutRequest
{
  public byte[] Version { get; set; } = null!;
  public string Name { get; set; } = null!;
  public string? Description { get; set; }
}

Edit: Oh, right, that cannot be done since HandleAsync signature is fixed..

maxkoshevoi commented 3 years ago

Okay, let's think about it in a different way. Why does Handle method needs to be predefined at all? We can just do this:

public class Put : BaseEndpointAsync
{
    [HttpPut("/investment-types/{Id:Guid}")]
    public async Task<ActionResult<InvestmentTypePutResponse>> HandleAsync(Guid id, InvestmentTypePutRequest request)
    {

    }
}

public class InvestmentTypePutRequest
{
  public byte[] Version { get; set; } = null!;
  public string Name { get; set; } = null!;
  public string? Description { get; set; }
}

And analyzer will keep us from defining multiple actions.

Side note:

maxkoshevoi commented 3 years ago

Answer 2:

Doesn't this section of readme addresses this exact question? https://github.com/ardalis/ApiEndpoints#how-can-i-bind-parameters-from-multiple-locations-to-my-model

ardalis commented 3 years ago

Original question seemed to indicate that wasn't working for him, but admittedly I don't have a reproduction of the problem, yet.

maxkoshevoi commented 3 years ago

Original question seemed to indicate that wasn't working for him

Maybe it's because [FromRoute] is missing from parameter's signature?

image

I haven't tried to reproduce it either, but I can say for sure that inheriting directly from BaseEndpointAsync would work.

ghost commented 3 years ago

This issue was created by Steve for me. I've created a VS solution with a repro at https://github.com/msantor/ApiEnpointsIssue124. I've included a Postman collection with the test call I'm using. It's possible I'm missing something very simple. Thank you for your time.

ghost commented 3 years ago

I resolved my problem. It was because I had multiple [FromBody] attributes directly on the request type. I created an additional class to represent the body of the request and model binding does the rest. Sorry to have wasted everyone's time on this. I needed to more thoroughly read through the documentation. Please close this issue.

ardalis commented 3 years ago

Ah, yes. That will do it. We've all been there. :)