Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to support OData query syntax but return non-Edm models

Exposing my EF models to an API always seemed wrong. I'd like my API to return a custom entity model to the caller but use EF on the back.

So I may have PersonRestEntity and a controller for CRUD ops against that and a Person EF code-first entity behind in and map values.

When I do this, I can no longer use the following to allow ~/people?$top=10 etc. in the URL

[EnableQuery]
public IQueryable<Person> Get(ODataQueryOptions<Person> query) { ... }

Because that exposes Person which is private DB implementation.

How can I have my cake and eat it?

like image 749
Luke Puplett Avatar asked Aug 28 '15 15:08

Luke Puplett


People also ask

What is EDM in OData?

EDM is short for Entity Data Model, it plays the role of a mapper between whatever data source and format you have and the OData engine.

What is $select in OData?

The $select option specifies a subset of properties to include in the response body. For example, to get only the name and price of each product, use the following query: Console Copy. GET http://localhost/odata/Products?$select=Price,Name.

What is Odataqueryoptions?

OData defines parameters that can be used to modify an OData query. The client sends these parameters in the query string of the request URI. For example, to sort the results, a client uses the $orderby parameter: http://localhost/Products?$orderby=Name. The OData specification calls these parameters query options.


1 Answers

I found a way. The trick is not to just return the IQueryable from the controller, because you need to materialise the query first. This doesn't mean materialising the whole set into RAM, the query is still run at the database, but by explicitly applying the query and materialising the results you can return mapped entities thereafter.

Define this action, specifying the DbSet entity type:

public async Task<HttpResponseMessage> Get(ODataQueryOptions<Person> oDataQuery)

And then apply the query manually to the DbSet<Person> like so:

var queryable = oDataQuery.ApplyTo(queryableDbSet);

Then use the following to run the query and turn the results into the collection of entities you publicly expose:

var list = await queryable.ToListAsync(cancellationToken);
return list
    .OfType<Person>()
    .Select(p => MyEntityMapper.MapToRestEntity(p));

Then you can return the list in an HttpResponseMessage as normal.

That's it, though obviously where the property names between the entities don't match or are absent on either class, there's going to be some issues, so its probably best to ensure the properties you want to include in query options are named the same in both entities.

Else, I guess you could choose to not support filters and just allow $top and $skip and impose a default order yourself. This can be achieved like so, making sure to order the queryable first, then skip, then top. Something like:

IQueryable queryable = people
    .GetQueryable(operationContext)
    .OrderBy(r => r.Name);

if (oDataQuery.Skip != null)
    queryable = oDataQuery.Skip.ApplyTo(queryable, new System.Web.OData.Query.ODataQuerySettings());                

if (oDataQuery.Top != null)
    queryable = oDataQuery.Top.ApplyTo(queryable, new System.Web.OData.Query.ODataQuerySettings());

var list = await queryable.ToListAsync(operationContext.CreateToken());

return list
    .OfType<Person>()
    .Select(i => this.BuildPersonEntity(i));

More information:

If you simply use the non-generic ODataQueryOptions you get

Cannot create an EDM model as the action 'Get' on controller 'People' has a return type 'System.Net.Http.HttpResponseMessage' that does not implement IEnumerable

And other errors occur under different circumstances.

like image 158
Luke Puplett Avatar answered Sep 30 '22 03:09

Luke Puplett