OData v4 order by multiple cardinality property

久未见 提交于 2020-01-25 04:38:05

问题


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:

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

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

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 alternative

NOTE: 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!