How to add post processing to .Net Core OData?

Viewed 413

I have an OData Controller which looks pretty standard.

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

Projection looks like this :

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

Now the problem is that I need to actually return a GridDataDTO object from this call. There is some processing that cannot be done at the database level. The processing is pretty heavy so I would not like to add it inside the GetGridDataQueryable().Also the processing is async, and need a materialized result set to be able to apply it.

I also need to return the IQueryable in the controller to be able to benefit from $count, $select, etc .

This hooks up to a pretty complex grid with a lot of options for filtering/sorting so I would not like to remove the OData functionality.

Is there a simple way to add postprocessing here ? After the result is materialized, project it to my GridDataDTO ?

There is no need for insert/update/delete support, as this will be only used for read operations.

1 Answers

There is no requirement for your controller method to only pass through a query from the database, in fact your method does not need to return an IQueryable<T> result at all!

You can still benefit from OData $select, $expand and $filter operators on result sets that are not IQueryable<T>, but you lose most of the performance benefits of doing so and you have to prepare you data so that the operators can be processed, and you will have to explicitly decorate your endpoint with the [EnableQuery] attribute.

In the following example you current query is materialized into memory, after applying the query options, then we can iterate over the set and manipulate it as we need to.
In the end the same recordset, with the modified records is returned, cast as queryable to match the method signature, however the method would still function the same if the result was IEnumerable<T>

There is a strong argument that says you should return IEnumerable<T> because it conveys the correct information that the recordset has been materialized and is not deferred.

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

UPDATE:

When the manipulations involve projection into a new type, then to properly support OData query options the type defined in your ODataQueryOptions<> needs to be assignable from the output element type. You can do this through inheritance or with implicit cast definitions.

If an explicit cast is required (or no cast is available at all) then you will have to manually validate the ApplyTo logic, the ODataQueryOptions must be a valid type reference to match the output.

Related