CreatedAtRoute, передающий несколько значений маршрута

#c# #asp.net-core #asp.net-core-5.0

#c# #asp.net-ядро #asp.net-core-5.0

Вопрос:

Я пытаюсь создать конечную точку (POST), которая принимает 1 или более моделей, которые затем будут массово вставлены в БД.

Все в порядке, за исключением случаев, когда я пытаюсь вызвать CreatedAtRoute список созданных объектов и маршрутов.

Когда я вызываю CreatedAtRoute с несколькими маршрутами / моделями, я получаю время выполнения InvalidOperationException , не совпадающие маршруты.

Пример

 [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 );
}
 

Для справки у меня есть следующая GET/{ID} конечная точка:

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

Вопрос

Возможно ли вернуть оба URI для доступа к вновь добавленным ресурсам, а также список модельного представления этих ресурсов?

Я также не уверен, что это правильный способ справиться с этой ситуацией (в отношении того, что я должен возвращать).

Комментарии:

1. CreatedAtRoute подразумевает один ресурс <-> один маршрут. Вероятно, вам потребуется вернуть код состояния и создать экземпляр объекта вручную

2. @CamiloTerevinto Да, я подумал, что так и будет. Является ли то, что я пытаюсь сделать (массовая вставка), необычным вариантом использования REST API?

3. В этом нет ничего необычного, но REST предназначен для работы с ресурсом (а не с ресурсами), поэтому вам нужно немного выйти из шаблона

Ответ №1:

Измените CreatedAtRoute , как показано ниже:

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

Измените GetQaiStateAsync метод, как показано ниже:

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

Ответ №2:

Чтобы заполнить заголовок location ответа 201, вернуть созданные модели и передать идентификаторы по маршруту, я должен был сделать следующее:

  1. Создайте новый GET обработчик, который ожидает массив идентификаторов
  2. Создайте a ModelBinder , который преобразует переданный массив идентификаторов из маршрута
  3. Укажите POST обработчикам CreatedAtRoute , чтобы они указывали на этот GET обработчик. Кроме того, вновь созданный ID's должен был быть объединен в список, разделенный запятыми, который передается в CreatedAtRoute

Шаг 1:

Теперь GET обработчик ожидает array , что через маршрут будет передан идентификатор. Обратите внимание на указанный маршрут ({ids}) . Здесь я указываю, что мне нужен массив значений, разделенных круглыми скобками () .

 [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 );
}
 

Шаг 2:

Для преобразования маршрута http://localhost:32003/QaiStateCollection/(4,2,3) в IEnumerable<int> пользовательскую модель необходимо было создать связующее.

 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;
    }
}
 

Шаг 3:

Здесь идентификаторы объединяются в список, разделенный запятыми, и передаются в CreatedAtRoute . Также передается новое GET имя обработчика 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 );
}  
 

Комментарии:

1. эммм, я думаю, что привязка вашей пользовательской модели немного сложна для ваших требований. Почему бы не попробовать мое решение ниже?

2. @Rena Потому что у меня есть другой контроллер, который имеет аналогичное требование, за исключением того, что идентификаторы являются Guid «s». Ваше решение не будет работать для этого. Тем не менее, я не указывал это в качестве требования, так что это не ваша вина.

3. Позвольте мне проверить. 🙂

4. @Rena Вы должны получить InvalidOperationException , если я правильно помню. Я все равно приму ваш ответ, поскольку он прямо ответил на мой первоначальный вопрос.

5. Привет @WBuck, я проверил это в своем проекте. Это сработало хорошо.эмммм, возможно, в вашем проекте отсутствуют какие-либо другие вещи. Я использовал asp.net ядро 3.1. Вы могли бы проверить gif:i.stack.imgur.com/iSQIl.gif