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....
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.