asp.net-core - OData v4 按多个基数属性排序
问题描述
我有以下网址:
{{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
我想从 TaskRuns(RunBy) 中按属性订购任务。TaskRuns 是一个我只想考虑第一项的集合。
我得到错误:"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 是一个 GUID 字段。RunAt 出现同样的问题,它是 DateTime 字段。我测试了其他场景,似乎问题是因为 TaskRuns 是一个集合。即使花费的时间比预期的要长,以下网址也可以工作:
{{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
后端:OData v4、asp 核心、v7.1.0
如何实现?谢谢!
解决方案
oData v4 规范不支持这种类型的嵌套排序。但这并不意味着您无法使用 oData 获得类似的结果集。
问题是我们想要对子集合内的一行中的 a 属性的顶级响应的结果进行排序。
这不受支持,您只能根据该集合中行的属性对每个集合进行排序。$orderby
只能引用单例属性路径对结果进行排序。您可以指定结果中记录的排序顺序$expand
,但 OOTB 不能使用集合中任意行的属性来指定顶级排序顺序。
您检索此类数据的选项分为 3 类:
在控制器上创建自定义端点以执行结果集
- 这应该是最容易实现的,但只有在您控制 API 时才可行。
- 上面的 Linq 查询非常简单,示例控制器函数可能如下所示:
[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)); }
不要害怕在你的控制器上创建自定义端点来实现特定的业务需求,你的 API 的目的是方便数据访问,是的 OData 为调用者公开了美妙的语法来完全控制要下载的数据,但你可以帮助他们出去。
- 更改您的查询以从 TaskRuns 控制器中检索
- API 端不需要进行任何更改
- 这会改变结果集和数据的结构
- 您可以使用按结果
$apply
返回组,这类似于原始要求 - 有关更多详细信息,请参见下文
$apply
- 覆盖
EnableQueryAttribute
或以其他方式实现您自己的$orderby
解释器- 这远远超出了本次讨论的范围,但是嘿,有可能,您可以实现自己的解释器
$orderby
以及如何将其应用于底层数据库查询,如果您有能力这样做,您应该考虑选项 1。 ..
- 这远远超出了本次讨论的范围,但是嘿,有可能,您可以实现自己的解释器
如果您不想或无法修改 API 本身,只有上面的选项 2 会为您提供帮助。
让我们探讨一下原因,例如,如果每个Task
都有多个TaskRuns
,TaskRuns
应该使用其中哪一个来获取RunBy
值?
您可能会考虑尝试 MIN 或 MAX 函数,但它们不会起作用,OData v4 规范中没有以下规定:
{{hostUrl}}/odata/TasksOData?$orderby=Max(TaskRuns/RunBy)
or
{{hostUrl}}/odata/TasksOData?$orderby=TaskRuns/Max(RunBy)
使用 $Apply
当您想根据子集合限制结果时,我们可以使用$apply
函数,但是类似的规则类型适用于只能对根集合进行操作。所以切换你的查询,这样子集合就是根!
即使使用
$apply
我也无法获得与原始查询所暗示的完全相同的结构或顺序,但此查询显示了另一种选择注意:如果您的日期字段不是,您将无法使聚合工作
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
应该返回 16 项(或更少),其结构类似于:
{
"@odata.context": …,
"@odata.count": 16,
"value": [
{
"@odata.id": null,
"RunAt": …,
"RunBy": …,
"Task": {
"@odata.id": null,
"Id": 7,
"Name": 'TaskName',
"State": 'Pending',
"LastChangedAt": …,
"LastChangedBy": …,
}
},
…
]
}