CreatedAtRoute passing multiple route values

901 Views Asked by At

I'm attempting to create an endpoint (POST) that accepts 1 or more models which will then be bulk inserted into the DB.

Everything is fine except when I attempt to call CreatedAtRoute with lists of the created objects and routes.

When I call CreatedAtRoute with multiple routes/models I get a runtime InvalidOperationException, no matching routes.

Example

[HttpPost]
 public async Task<ActionResult<IEnumerable<QaiStateModel>>> CreateQaiStateAsync( 
        IEnumerable<QaiStateCreationModel> inputQaiStates )
 {            
     var qaiStates = _mapper.Map<IEnumerable<QaiState>>( inputQaiStates );
     await _qaiService.AddQaiStatesAsync( qaiState ).ConfigureAwait( false );

     var models = _mapper.Map<List<QaiStateModel>>( qaiState );

     var ids = models.Select( m => new { qaiStateID = m.ID } );
     return CreatedAtRoute( "GetQaiState", ids, models );
}

For reference I do have the following GET/{ID} endpoint:

[HttpGet( "{qaiStateID}", Name = "GetQaiState" )]
public async Task<ActionResult<QaiStateModel>> GetQaiStateAsync( int qaiStateID )

Question

Is it possible to return both URI's to access the newly added resources as well as a list of the model representation of those resources?

I'm also unsure if this is the correct way to handle this situation (in regards to what I should be returning).

2

There are 2 best solutions below

0
On BEST ANSWER

Change CreatedAtRoute like below:

return CreatedAtRoute("GetQaiState", new { qaiStateID = ids },models);

Change GetQaiStateAsync method like below:

public async Task<ActionResult<QaiStateModel>> GetQaiStateAsync(List<int> qaiStateID )
6
On

In order to populate the location header of the 201 response, return the created models and pass the ID's via the route I had to do the following:

  1. Create a new GET handler that expects an array of ID's
  2. Create a ModelBinder that converts the passed array of ID's from the route
  3. Point the POST handlers CreatedAtRoute to point to this GET handler. Furthermore the newly created ID's had to be concatenated in to a comma separated list which gets passed to CreatedAtRoute

Step 1:

The GET handler now expects an array of ID's to be passed via the route. Note the specified route ({ids}). Here I'm specifying I want an array of values delimited with parenthesis ().

[HttpGet( "({ids})", Name = "GetQaiStateCollection" )]
public async Task<ActionResult<IEnumerable<QaiStateModel>>> GetQaiStateCollectionAsync( 
        [FromRoute]
        [ModelBinder( BinderType = typeof( ArrayModelBinder ) )]
        IEnumerable<int>? ids )
{
    if ( ids is null )
        return BadRequest( "Invalid ID's" );

    var states = await _qaiService
        .GetQaiStatesAsync( ids ).ConfigureAwait( false );

    if ( ids.Count( ) != states.Count( ) )
        return NotFound( "Failed to find one or more states matching the provided ID's" );

    var models = _mapper.Map<IEnumerable<QaiStateModel>>( states );
    return Ok( models );
}

Step 2:

In order to convert the route http://localhost:32003/QaiStateCollection/(4,2,3) into a IEnumerable<int> a custom model binder had to be created.

public class ArrayModelBinder : IModelBinder
{
    public Task BindModelAsync( ModelBindingContext bindingContext )
    {
        // This model binder only works for enumerable types.
        if ( !bindingContext.ModelMetadata.IsEnumerableType )
        {
            bindingContext.Result = ModelBindingResult.Failed( );
            return Task.CompletedTask;
        }

        // Get the input value via the value provider.
        var value = bindingContext.ValueProvider
            .GetValue( bindingContext.ModelName ).ToString( );
        
        if ( string.IsNullOrWhiteSpace( value ) )
        {
            // Returning null here will allow us to check for this
            // and return a bad request.
            bindingContext.Result = ModelBindingResult.Success( null );
            return Task.CompletedTask;
        }

        var elementType = bindingContext.ModelType.GenericTypeArguments.First( );
        var converter = TypeDescriptor.GetConverter( elementType );
        
        var values = value.Split( new [ ] { "," }, StringSplitOptions.RemoveEmptyEntries )
            .Select( v => converter.ConvertFromString( v.Trim( ) ) )
            .ToArray( );

        var typedValues = Array.CreateInstance( elementType, values.Length );
        values.CopyTo( typedValues, 0 );
        bindingContext.Model = typedValues;

        bindingContext.Result = ModelBindingResult.Success( bindingContext.Model );
        return Task.CompletedTask;
    }
}

Step 3:

Here the ID's are concatenated into a comma separated list and passed to the CreatedAtRoute. The new GET handler name is also passed to CreatedAtRoute.

[HttpPost]
public async Task<ActionResult<IEnumerable<QaiStateModel>>> CreateQaiStateCollectionAsync( 
           IEnumerable<QaiStateCreationModel> inputModels )
{            
    var qaiStates = _mapper.Map<IEnumerable<QaiState>>( inputModels );
    await _qaiService.AddQaiStateCollectionAsync( qaiStates ).ConfigureAwait( false );

    var models = _mapper.Map<IEnumerable<QaiStateModel>>( qaiStates );
    var ids = string.Join( ",", models.Select( m => m.ID ) );

    return CreatedAtRoute( "GetQaiStateCollection", new { ids }, models );
}