ardalis / ApiEndpoints

A project for supporting API Endpoints in ASP.NET Core web applications.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Enable IActionResult Response

pdevito3 opened this issue · comments

Research For Duplicates

I looked through the issues and docs and didn't see anything discussing this. Apologies if it's there and I missed it.

Describe the feature you'd like

So I love the idea of breaking my controller out like this and started updating my api template to use API Endpoints. Once I started though, I noticed that, as far as I can tell, I have to chose an explicit response for my endpoints that previously use IActionResult as described in the MS docs. This seems to be due to the WithResponse method wrapping whatever is passed to it in a Task<ActionResult<>>. This is obviously by design and I'm sure I can get things to at least working as is, but it seems like we should have the option here to do things, as far as I know, semantically correct.

Example

Here's an example for a POST.

    [Route("/api/patients")]
    public class PostPatientEndpoint : BaseAsyncEndpoint
        .WithRequest<PatientForCreationDto>
        .WithResponse<IActionResult>
    {
        private readonly IMediator _mediator;

        public PostPatientEndpoint(IMediator mediator)
        {
            _mediator = mediator;
        }

        /// <response code="201">Patient created.</response>
        /// <response code="400">Patient has missing/invalid values.</response>
        /// <response code="409">A record already exists with this primary key.</response>
        /// <response code="500">There was an error on the server while creating the Patient.</response>
        [ProducesResponseType(typeof(Response<PatientDto>), 201)]
        [ProducesResponseType(typeof(Response<>), 400)]
        [ProducesResponseType(typeof(Response<>), 409)]
        [ProducesResponseType(500)]
        [Consumes("application/json")]
        [Produces("application/json")]
        [SwaggerOperation(
            Summary = "Creates a new Patient record.",
            OperationId = "PostPatientEndpoint",
            Tags = new[] { "Patients" })
        ]
        [HttpPost, MapToApiVersion("1.0")]
        public override async Task<IActionResult> HandleAsync([FromBody] PatientForCreationDto patientForCreation,
            CancellationToken cancellationToken = new CancellationToken())
        {
            // add error handling
            var command = new AddPatient.AddPatientCommand(patientForCreation);
            var commandResponse = await _mediator.Send(command);
            var response = new Response<PatientDto>(commandResponse);

            return CreatedAtRoute("GetPatient",
                new { commandResponse.PatientId },
                response);
        }
    }

You can use WithoutResponse for this (misleading name in this case, but it will work).
This will create a Task<ActionResult> method that is able to return CreatedAtRoute.

PS: I think, WithoutResponse should really be named WithActionResponse (or WithActionResult), and WithoutResponse should result in Task method (without type argument).

Haven't tried this yet, but even if that does work, how do you resolve the route argument to point to the get route for getting the data from the POST (i.e. GetPatient)?

Didn't quite understand you there, but

return CreatedAtRoute("GetPatient",
	  new { commandResponse.PatientId },
	  response);

will work, if that's what you asking.

Didn't quite understand you there, but

return CreatedAtRoute("GetPatient",

	  new { commandResponse.PatientId },

	  response);

will work, if that's what you asking.

This was more of a follow up.

I don't have a chance to try it right now, but last I remember it didn't work because it doesn't know where GetPatient is and I still don't see how it could know, unless ApiEndpoint has some key mapping under the hood across all routes. The only way this currently works to my knowledge is if is this POST is in the same controller as GetPatient

Can you show how GetPatient controller/action looks? More specifically, what is the route to it and what arguments it accepts.

This is supported in 4.x release, I believe. We now have native ActionResult and ActionResult options with the fluent generic base class, as well as ability to just use the base class directly with whatever signature you want.