问题
I have the following url:
{{hostUrl}}/odata/TasksOData?$select=Id,Name,State,TaskRuns,LastChangedAt,LastChangedBy&$expand=TaskRuns($orderby=RunAt desc;$top=1;$select=Status,RunAt,RunBy)&$top=16&$orderby=TaskRuns/RunBy
I want to order Tasks by property from TaskRuns(RunBy). TaskRuns being a collection I want to take into consideration only first item.
I get error: "message": "The parent value for a property access of a property 'RunBy' is not a single value. Property access can only be applied to a single value.",
RunBy is a GUID field. The same issue occurs for RunAt which is DateTime field. I tested with other scenarios and seems that the issue is because TaskRuns is a collection. The following url works even if takes longer than expected:
{{hostUrl}}/odata/TasksOData?$select=Id,Name,State,TaskRuns,LastChangedAt,LastChangedBy&$expand=TaskRuns($orderby=RunAt desc;$top=1;$select=Status),Script($select=Name)&$top=16&$orderby=Script/Name desc
Backend: OData v4, asp core, v7.1.0
How to achieve it? Thanks!
回答1:
This type of nested sorting is not supported by the oData v4 spec. But that does not mean that you cannot achieve a similar resultset using oData.
The issue is that we want to sort the results of the top level response on the a property from a row inside a child collection.
This isn't supported, you can only sort each collection based on properties of rows in that collection. $orderby
can only reference singleton property paths to sort the results. You can specify the sort order of records in $expand
results, but OOTB you cannot use a property from an arbitrary row within a collection to specify the top level sort order.
Your options for retrieving this type of data fall into 3 catergories:
Create a Custom Endpoint on your Controller to execute the resultset
- This should be the easiest to implement, but is only viable if you control the API.
- Linq query to the above is quite simple, an example controller function could look something like this:
[HttpGet] public IHttpActionResult GetRecentTaskRuns() { var query = from task in _db.Tasks let lastRun = task.TaskRuns.OrderByDescending(t => t.RunAt).FirstOrDefault() select new { task.Id, task.Name, task.State, // Consider not returning this as a collection at all //TaskRuns = task.TaskRuns.OrderByDescending(t => t.RunAt).Take(1).ToArray(), LastChangedAt, LastChangedBy, // Just return LastRun, instead of TaskRuns. LastRun = lastRun }; return Json(query.OrderBy(t => t.LastRun.RunBy)); }
Don't be afraid to create custom endpoints on your controllers to achieve specific business needs, the purpose of your API is to facilitate data access, yes OData exposes wonderful syntax for the caller to fully control what data to download, but you can help them out.
- Change your Query to retrieve from the TaskRuns controller instead
- No changes should be needed on the API side
- This changes both the structure of the resultset and data
- You can use
$apply
to return a group by result, that would be similar to the original requirement - See below for more details on
$apply
- Override
EnableQueryAttribute
or otherwise implement your own$orderby
interpreter- This is far outside the scope of this discussion, but hey, it's possible, you can implement your own interpreter for
$orderby
and how it is applied to the underlying database query, if you have the ability to do this though, you should consider option 1...
- This is far outside the scope of this discussion, but hey, it's possible, you can implement your own interpreter for
Only option 2 above will assist you if you do not want to, or are unable to modify the API itself.
Lets explore the reasons, for instance, if each Task
has multiple TaskRuns
, which one of those TaskRuns
should be used to obtain the RunBy
value?
You might think to try MIN or MAX functions, but they wont work, there is no provision within the OData v4 specification for the following:
{{hostUrl}}/odata/TasksOData?$orderby=Max(TaskRuns/RunBy)
or
{{hostUrl}}/odata/TasksOData?$orderby=TaskRuns/Max(RunBy)
Using $Apply
When you want to limit the results based on the child collection we could use an $apply
function, however a similar rule sort of applies about only being able to operate on the root collection. So switch your query around so the child collection IS the root!
even using
$apply
I can't get the exact same structure or ordering as your original query implies, but this query shows an alternativeNOTE: You wont be able to get aggregates to work if your date fields are not
DateTimeOffset
{{hostUrl}}/odata/TaskRunsOData?
$apply=groupby((Task/Id,Task/Name,Task/State,Task/LastChangedAt,Task/LastChangedBy), aggregate(RunAt with max as RunAt,RunBy with max as RunBy)&$top=16&$orderby=RunBy DESC&$count=true
Should return 16 item (or less) with a structure similar to this:
{
"@odata.context": …,
"@odata.count": 16,
"value": [
{
"@odata.id": null,
"RunAt": …,
"RunBy": …,
"Task": {
"@odata.id": null,
"Id": 7,
"Name": 'TaskName',
"State": 'Pending',
"LastChangedAt": …,
"LastChangedBy": …,
}
},
…
]
}
来源:https://stackoverflow.com/questions/55300166/odata-v4-order-by-multiple-cardinality-property