2
votes

I'm building an OData v.4 web service that must expose data retrieved from another 3rd party web source, so the data doesn't resemble anything from LINQ world, i.e.: no IQueryable, no Context, no whatever..

The way to go seems to be manually processing parameters from ODataQueryOptions and returning simple sequence of items. So, controller method should look something like this:

class MyMasterEntity {
    [Contained]
    public IEnumerable<MyDetailEntity> Details { get; set; }
}

// [EnableQuery]
public IEnumerable<MyMasterEntity> Get(ODataQueryOptions<MyMasterEntity> options)
{
   // process .FilterQueryOption
   // process .SelectExpandQueryOption
   // process .SkipQueryOption
   // process .TopQueryOption
   return myMasterEntityList;
}

This works well except for $expand=Details, in which case properties are not expanded in resulting response, although my logic adds them just fine.

If adding [EnableQuery] attribute (which makes no sense to start with, as it's mutually exclusive with the whole idea of ODataQueryOptions), then expansion starts working. Or it rather pretends to be working, because what is really happening is that query is processed twice: first time by my code, then by OData machinery after i returned the data. This could be tolerable (anyway, i do expensive calls manually, so no big deal of OData retrying on already prepared data), if not for the fact that that second pass screws non-deterministic operations like $skip. (I.e.: it's okay to apply $top as many times as you like receiving the same result, but it's not okay to do this with $skip).

As i understood from reverse engineering relevant assemblies, standard expansion code wraps entities into something, which tells JSON formatter to emit corresponding properties, regardless if they're actually expanded inside entities.

Also tried:

  • changing return type (IQueryable, IHttpActionResult)
  • force calling SelectExpandQueryOption.ApplyTo(myMasterEntityList, new ODataQuerySettings()) after manual expansion, but before returning

How do I properly expand navigation properties?

2

2 Answers

2
votes

I believe you need to introduce a piece of code as follows

if(options.SelectExpand != null)
{
    Request.ODataProperties().SelectExpandClause = options.SelectExpand.SelectExpandClause;
}

The ODataProperties extesion method is defined in namespace System.Web.OData.Extensions.

This tells the OData formatter to render also the select and expand to output. It can't touch the expanded properties otherwise as it could trigger an enumeration. At least this is how I understand it.

Otherwise it looks like your approach is a sound one. Works for me, so to speak. My case is that I have a database first model I can't augment (e.g. relations between views), so I augment the OData model and, for instance, expand by hand the "foreign key links" and one-to-many relations otherwise not in the declared edmx model. Then when possible, I would like the OData query to hit DB level (e.g filter), so what I do is roughly as follows

public async Task<IHttpActionResult> Get(ODataQueryOptions<T> options)
{
    IQueryable<T> tempQuery = initialQuery; //E.g. EfContext.T
    IQueryable<T result = tempQuery;

    if(options.SelectExpand != null)
    {
        if(options.Filter != null)
        {
            tempQuery = options.Filter.ApplyTo(tempQuery, new ODataQuerySettings()) as IQueryable<T>;   
        }
        /* Other options that should go to the DB level... */

        //The results need to be materialized, or otherwise EF throws a
        //NotSupportedException due to columns and properties that aren't in the DB...
        result = (await tempQuery.ToListAsync()).AsQueryable();

        //Do here the queries that can't be hit straight by the queries. E.g.
        //navigation properties not defined in EF views (that can't be augmented)...

        //This is needed to that OData formatter knows to render navigation links too.
        Request.ODataProperties().SelectExpandClause = options.SelectExpand.SelectExpandClause;
}

return Ok(result);
4
votes

Why do you want to process the query options manually ?

Use the AsQueryable LINQ extension method to tranform your data as a queryable collection (in fact you are using LINQ to Object).

  • Your controller must inherit from ODataController
  • An IEdmModel must be associated with the route. Define it using the ODataModelBuilder (or ODataConventionModelBuilder).
  • Your method must return IQueryable<MyMasterEntity>
  • Declare just the [EnableQuery]. Do not declare explicit ODataQueryOptions argument