ardalis / ApiEndpoints

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

Allow for multiple parameters on an endpoint #199

Open rikrak opened 1 year ago

rikrak commented 1 year ago

Not sure if this is currently possible, but I've been trying to create a POST endpoint where part of the parameters come from the route. I can't get it to work with a single request parameter as the entire class is bound to the requets body.

For example, I'd like to be able to support scenarios like:

    [Route("/Country/{id}/Rename")]
    [HttpPost]
    public override async Task<ActionResult> HandleAsync(CountryIdentifier id, CountryRenameRequest request, CancellationToken cancellationToken = new CancellationToken())
    {
    }

This doen't seem to be currently possible if I use the EndpointBasAsync.WithRequest<T>.WithResponse mechanism for declaring the endpoint.

I've worked around it by re-creating the EndpointBaseAsync structure and adapting it. Is this an appropriate thing to do, or is there a better way to achieve the same thing?

    public static class EndpointAsync
    {
         public static class WithRequest<TParam, TRequest>
        {
            public abstract class WithResult<TResponse> : EndpointBase
            {
                public abstract Task<TResponse> HandleAsync(
                    TParam param,
                    TRequest request,
                    CancellationToken cancellationToken = default(CancellationToken)
                    );
            }

            public abstract class WithoutResult : EndpointBase
            {
                public abstract Task HandleAsync(
                    TParam param, 
                    TRequest request, 
                    CancellationToken cancellationToken = default(CancellationToken));
            }

            public abstract class WithActionResult<TResponse> : EndpointBase
            {
                public abstract Task<ActionResult<TResponse>> HandleAsync(
                    TParam param,
                    TRequest request,
                    CancellationToken cancellationToken = default(CancellationToken));
            }

            public abstract class WithActionResult : EndpointBase
            {
                public abstract Task<ActionResult> HandleAsync(
                    TParam param,
                    TRequest request,
                    CancellationToken cancellationToken = default(CancellationToken));
            }

            public abstract class WithAsyncEnumerableResult<T> : EndpointBase
            {
                public abstract IAsyncEnumerable<T> HandleAsync(
                    TParam param,
                    TRequest request,
                    CancellationToken cancellationToken = default(CancellationToken));
            }
        }
        // ... other code (truncated for clarity)
}

happy to submit a PR for this sort of thing :-)

rikrak commented 1 year ago

Actually, having just reviewed this again in my codebase, I'm not sure I like the idea of multiple parameters. It makes the route intuitive, but it marginally complicates the construction of the command.

    public override async Task<ActionResult> HandleAsync(CountryIdentifier id, CountryRenameRequest request, CancellationToken cancellationToken = new CancellationToken())
    {
        var cmd = _mapper.Map<CountryRenameCommand>(request);
        cmd = cmd with {Id = id};
        RunCommand(cmd);
        // ...
    }

instead of

    public override async Task<ActionResult> HandleAsync(CountryRenameRequest request, CancellationToken cancellationToken = new CancellationToken())
    {
        var cmd = _mapper.Map<CountryRenameCommand>(request);
        RunCommand(cmd);
        // ...
    }