Как добавить постобработку в .Net Core OData?

#asp.net-core #odata

#asp.net-core #odata

Вопрос:

У меня есть контроллер OData, который выглядит довольно стандартно.

 [HttpGet]
[ODataRoute("GridData")]
[EnableQuery]
public async Task<IQueryable<GridData>> GetGridData(ODataQueryOptions<GridData> odataOptions)
{
    var query = odataOptions.ApplyTo(_service.GetGridDataQueryable()) as IQueryable<GridData>
    return query;
}
  

Проекция выглядит следующим образом :

 .Select(async x =>
{
   //Pretty resource heavy
   x.Ownership = await _ownershipService.ComputeAsync(_currentUser)); 
   return x;
})
.Select(t => t.Result)
.ToList();
  

Теперь проблема в том, что мне нужно фактически вернуть объект GridDataDTO из этого вызова. Существует некоторая обработка, которая не может быть выполнена на уровне базы данных. Обработка довольно тяжелая, поэтому я бы не хотел добавлять ее в GetGridDataQueryable().Кроме того, обработка выполняется асинхронно, и для ее применения требуется материализованный набор результатов.

Мне также нужно вернуть IQueryable в контроллере, чтобы иметь возможность извлекать выгоду из $count, $ select и т.д.

Это подключается к довольно сложной сетке с множеством опций для фильтрации / сортировки, поэтому я бы не хотел удалять функциональность OData.

Есть ли простой способ добавить постобработку здесь? После того, как результат материализуется, спроецируйте его на мой GridDataDTO ?

Нет необходимости в поддержке вставки / обновления / удаления, поскольку это будет использоваться только для операций чтения.

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

1. Не зная структуры рассматриваемых объектов, мы не можем легко предложить вам явные решения, но как только вы материализуете набор записей, возможно, с помощью . С помощью функции ToList() вы можете выполнять любые необходимые манипуляции, прежде чем возвращать набор или совершенно новый.

2. Проблема с . ToList() заключается в том, что он не будет поддерживать вызовы $ count таким образом. Ничего не возвращается в терминах «@odata.*****». Кажется, мне нужно позволить процессору OData творить чудеса, а затем применить постобработку к результату.

3. Если вы выполняете постобработку, $ count на самом деле больше не может означать столько же, это $ count до или после обработки набора записей? Вы можете подделать результат IQueryable из списка в памяти, но $count теперь может применяться только к измененному набору результатов, а не к базовой таблице, которая может не подходить для всех приложений

4. Манипуляция выполняется с точки зрения проецирования на другой DTO, изменяя класс. Количество элементов не изменилось.

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

Ответ №1:

Для вашего метода контроллера не требуется передавать только запрос из базы данных, фактически вашему методу вообще не нужно возвращать IQueryable<T> результат!

Вы все еще можете извлечь выгоду из OData $select , $expand и $filter операторов для наборов результатов, которые не являются IQueryable<T> , но вы теряете большую часть преимуществ в производительности от этого, и вам нужно подготовить данные, чтобы операторы могли быть обработаны, и вам придется явно украсить свою конечную точку [EnableQuery] атрибутом.

В следующем примере ваш текущий запрос материализуется в памяти, после применения параметров запроса мы можем выполнить итерацию по набору и манипулировать им по мере необходимости.
В конце возвращается тот же набор записей с измененными записями, приведенными как запрашиваемые для соответствия сигнатуре метода, однако метод все равно будет функционировать так же, если результат был IEnumerable<T>

Существует веский аргумент, который гласит, что вы должны вернуть IEnumerable<T> , потому что он передает правильную информацию о том, что набор записей был реализован и не отложен.

 [HttpGet]
[ODataRoute("GridData")]
public async Task<IQueryable<GridDataDTO>> GetGridData(ODataQueryOptions<GridData> odataOptions)
{
    // NOTE: GridDataDTO : GridData
    // apply $filter, $top and $skip to the DB query
    IQueryable<GridData> query = odataOptions.ApplyTo(_service.GetGridDataQueryable());
    // materialize
    var list = query.ToList();

    // project into DTO
    List<GridDataDTO> output = list.Select(async x =>
    {
        var o = new GridDataDTO(x);
        o.Ownership = await _ownershipService.ComputeAsync(_currentUser));
    }).ToList();
    
    // return, as Queryable
    return output.AsQueryable();
}
  

Обновить:

Когда манипуляции включают проекцию в новый тип, то для правильной поддержки параметров запроса OData тип, определенный в вашем, ODataQueryOptions<> должен быть присваиваемым из типа выходного элемента. Вы можете сделать это с помощью наследования или с помощью неявных определений приведения.

Если требуется явное приведение (или приведение вообще не доступно), вам придется вручную проверять ApplyTo логику, ODataQueryOptions должна быть допустимая ссылка на тип, чтобы соответствовать результату.

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

1. Проблема, с которой я сталкиваюсь при таком подходе, заключается в том, что фильтрация / top / skip будет применена только после завершения работы метода. Это приведет к действительно плохой производительности, поскольку манипуляции будут выполняться со всем набором из базы данных.

2. Вы не описали свою логику, поэтому сложно дать вам универсальное решение для всех. Вы можете применить $top, $ skip, $ filter перед вашей проекцией, именно это делает .ApplyTo вызов, но если вы примените сначала, то ваше выражение $ filter должно работать с неизмененными полями. При необходимости можно также применять различные параметры запроса независимо или полностью вручную. Вам нужен более конкретный пример кода, если вы хотите более конкретное решение.

3. Другой вариант — применить проекции исключительно в linq, возможно, возвращая другой DTO или начиная с другого запроса данных. Это, безусловно, можно сделать, но решение будет зависеть от вашего запроса, параметров OData, которые вы хотите поддерживать, и характера ваших прогнозов.

4. Думал, что будет простой способ применить эту постобработку (какой бы она ни была) после того, как конвейер OData завершит свою работу. Я не хочу переписывать фильтрацию OData в Linq. Что-то вроде перехватчика.

5.Вы упускаете суть, в первой строке odataOptions.ApplyTo ЭТО применение операторов входящих запросов к отложенному запросу. Он максимально оптимизирован. То, что вы пытаетесь сделать, в целом довольно просто, и да, вы можете использовать перехватчик, но мне это нужно было только в очень небольшом наборе очень специфичных сценариев. Не забывайте, что постобработка — это, как правило, компромисс между тем, сколько манипуляций вам НУЖНО выполнить на сервере, и тем, сколько из них легче выполнить на клиенте.