Search code examples
asp.net-web-apiodata

How do you enable $select in a web api OData query


How do you enable web api to provide data that is queryable with the $select OData operator?

I'm using Web API 2.2 (or Microsort.AspNet.WebApi 5.2.2) I'm not using EF and my backend is asynchronous and doesn't support IQueryable. I don't mind querying the entire data set and filtering on the web-server before passing off to the client.

What I have below isn't ideal because it returns a Task<IQuerable...> however I don't know how to do it another way, and in fact don't know how to do it at all.

The following code works, however throws an error when using $select (let's call it code block A):

[EnableQuery(HandleNullPropagation = HandleNullPropagationOption.True)]
public async Task<IQueryable<Cars>> GetCars(ODataQueryOptions<Cars> queryOptions)
{
    // validate the query.
    try
    {
        queryOptions.Validate(_validationSettings);
    }
    catch (ODataException ex)
    {
        throw new HttpRequestException(ex.Message);
    }

    var result = await _context.GetCarsAsync();
    var queryableResult = queryOptions.ApplyTo(result.AsQueryable()) as IQueryable<Cars>;

    return queryableResult;
}

The following code doesn't work and returns a 406 (let's call it code block B):

[EnableQuery(HandleNullPropagation = HandleNullPropagationOption.True)]
public async Task<IQueryable> GetCars(ODataQueryOptions<Cars> queryOptions)
{
    // validate the query.
    try
    {
        queryOptions.Validate(_validationSettings);
    }
    catch (ODataException ex)
    {
        throw new HttpRequestException(ex.Message);
    }

    var result = await _context.GetCarsAsync();
    var queryableResult = queryOptions.ApplyTo(result.AsQueryable());

    return queryableResult;
}

I guess the latter code doesn't work because it isn't returning a strongly typed object and somehow the serialization engine can't handle this. I was trying to do this and I also tried replacing Cars in code block A with dynamic to enable $select, but neither work.

So two questions:

  1. Is there any way to enable $select, considering my backend doesn't support IQueryable? (Without digging into the IQueryable interface itself)

  2. What is the 'correct' way to do this without returning a Task<IQuerable...>?

UPDATE - @Marvin Smit (Error - Code Block A when using $select)

The full error when trying to project the code block A using $select is below. Example /Cars?$select=NumberPlateBasically it's a serialization error (something to do with wrapping IQueryable in a Task?). Putting a break point in shows that the data has successfully been retrieved and projected after the queryOptions.ApplyTo(... and the error only occurs after returning.

<m:error xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <m:code/>
  <m:message xml:lang="en-US">An error has occurred.</m:message>
  <m:innererror>
    <m:message>
      The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; charset=utf-8'.
    </m:message>
    <m:type>System.InvalidOperationException</m:type>
    <m:stacktrace/>
    <m:internalexception>
      <m:message>Cannot serialize a null 'feed'.</m:message>
      <m:type>
        System.Runtime.Serialization.SerializationException
      </m:type>
      <m:stacktrace>
        at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObjectInline(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
         at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)
         at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
         --- End of stack trace from previous location where exception was thrown ---
         at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
         at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
         at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
         at System.Web.Http.WebHost.HttpControllerHandler.
        <WriteBufferedResponseContentAsync>d__1b.MoveNext()
      </m:stacktrace>
    </m:internalexception>
  </m:innererror>
</m:error>

Solution

  • The problem is not linked to Tasks.

    After some research the problem is this line of code in block A:

    queryOptions.ApplyTo(result.AsQueryable()) as IQueryable<Cars>
    

    queryOptions.ApplyTo returns IQueryable which is not of type IQueryable<Cars> and so you can't just cast it. The result is null and you end up with this error. There doesn't seem to be a method queryOptions.ApplyTo<T> returning IQueryable<T>. Check this and the linked website for some explanation about the two.

    You actually don't need to apply the query options to make the odata filters work. Just using this code will work.

    [EnableQuery(HandleNullPropagation = HandleNullPropagationOption.True)]
    public async Task<IQueryable<Cars>> GetCars(ODataQueryOptions<Cars> queryOptions)
    {
         // validate the query.
         try
         {
            queryOptions.Validate(_validationSettings);
         }
         catch (ODataException ex)
         {
             throw new HttpRequestException(ex.Message);
         }
    
         var result = await _context.GetCarsAsync();
         return result.AsQueryable();
     }
    

    The filters are automatically applied by asp.net odata implementation after your method returns.

    The applyTo is useful if you want to apply the query options in your code and then return something other than IQueryable (example here in scenario 9).

    Concerning the serialization error in Block B (after having added application/json) I don't know.