Search code examples
c#entity-frameworkodataodata-v4

OData-v4 - operating on an entity collection then performing a Function


In my ODATA-v4 controller, I have the following code:

var fn = reportModelBuilder.EntityType<CurrentTestResult>()
         .Collection.Function("Breakdown").Returns<int>();

In the CurrentTestResultController.cs, I have the deceptively simple:

[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
[HttpGet]
public IHttpActionResult Breakdown()
    {
    var count = dataService.GetAll()
                .Select(x => x.TestResultTypeId)
                .Distinct()
                .Count();

    return Ok(count);
    }

Essentially, for all the CurrentTestResult entities in the collection, it returns the distinct TestResultTypeId occuring in the set. (This is a trivial operation, but I simplified a really life scenario that's much more complex)

That was easy to do - but I can't seem to first filter the collection of CurrentTestResult it should operate on.

This request, which by default operates on all CurrentTestResult entities

localhost/app/odatareport/CurrentTestResult/Default.Breakdown

returns

{
@odata.context: "http://localhost/app/odatareport/$metadata#Edm.Int32",
value: 5
}

(A correct result, there's 5 distinct types)

However, this request, which tries to simply filter it down first - fails

localhost/app/odatareport/CurrentTestResult/Default.Breakdown?$top=2

returns

{
error: {
code: "",
message: "The query specified in the URI is not valid. The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.",
innererror: {
message: "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.",
type: "Microsoft.OData.ODataException",
stacktrace: 
" at System.Web.OData.EnableQueryAttribute.ValidateSelectExpandOnly(ODataQueryOptions queryOptions) at System.Web.OData.EnableQueryAttribute.ExecuteQuery(Object response, HttpRequestMessage request, HttpActionDescriptor actionDescriptor, ODataQueryContext queryContext) at System.Web.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)"
     }
  }
}

As far as I understand the ODATA pipeline, why this fails makes sense. The controller method will return an IQueryable and then the ODATA $filter, $top, etc.. will be applied.

I would like a function to operate on a set that's already been filtered down.

Is what I am trying to do even possible?

I get that the Breakdown method itself has .GetAll() in it, but there mut be a way to apply the filtering before the method -

Otherwise, this is all quite pointless....


Solution

  • Two options:

    1) ODATA has a $count endpoint (see this but there are two forms of $count - an endpoint api/collection/$count and a system query option api/collection?$count=true; you'd want the endpoint) that returns the count of a collection (which can be filtered with EnableQuery). Treat your function as any other GET collection method and return the query you wish to have counted (in this case, distinct TestResultTypeId), then simply require a client to request its $count endpoint.

    2) define an ODataQueryOptions<T> parameter for your action method and apply the options manually:

    public IHttpActionResult Get( ODataQueryOptions<CurrentTestResult> queryOptions )
    {
        // base query with ODataQueryOptions applied
        var query = queryOptions.ApplyTo( dataServcie.GetAll() ) 
            as IQueryable<CurrentTestResult>;
    
        // get distinct TestResultTypeId's and then count
        var count = query.Select(x => x.TestResultTypeId)
                .Distinct()
                .Count();
    
        return Ok( count );
    }
    

    I'm mobile right now so not able to test, but this should be close enough (if not accurate) to get you where you need to go. Please let me know if there are any issues and I'll updated the answer.