WebAPI OData v4 查询不是异步的?
Posted
技术标签:
【中文标题】WebAPI OData v4 查询不是异步的?【英文标题】:WebAPI OData v4 queries not async? 【发布时间】:2016-06-25 10:34:50 【问题描述】:我一直在四处寻找这个问题的答案,但我似乎找不到任何明确的答案。 我们使用 OData v4,使用 ODataQueryOptions.ApplyTo 将 OData 选项应用于查询。我们还使用 ODataQuerySettings 来设置 pagesize。当我们设置页面大小时,我们不能再对从 ODataQueryOptions.ApplyTo 返回的 IQueryable 使用 ToListAsync()。错误消息说 IQueryable 的提供者不再来自实体框架。
我发现这是因为当使用 pagesize 时,OData 通过将 IQueryable 传递给 TruncatedCollection 来解析它。此 TruncatedCollection 从数据库中检索所有 (pagesize + 1) 个结果,以检查是否有超过 pagesize 个结果。但是,ApplyTo 不是异步方法,所以我可以放心地假设这个数据库查询不是异步执行的。
我可以做些什么来确保异步执行查询吗? OData 团队肯定想到了这一点吗?还是可以保持同步?在我看来,现在异步 IO 几乎是必需品,因为我们希望 API 能够很好地扩展,并且在等待 IO 时不会阻塞所有线程。
感谢您的帮助!
编辑 1:
我被要求提供一些代码来解释我的意思。
在 BaseController.cs 中:
public class BaseController : ODataController
private static readonly ODataQuerySettings DefaultSettings = new ODataQuerySettings() PageSize = 60 ;
protected Task<IHttpActionResult> ODataResult<T>(IQueryable<T> query, ODataQueryOptions<T> options)
IQueryable result = options.ApplyTo(query, DefaultSettings);
return Task.FromResult(ODataOk(result));
在 CustomerController.cs 中:
public class CustomerController : BaseController
ICustomerService customerService;
public async Task<IHttpActionResult> Get(ODataQueryOptions<Customer> options)
var query = customerService.Query();
return await ODataResult(query, options);
正如我上面所说,问题出在 ApplyTo 的底层代码中。这是 OData 本身的一种方法。 行:
IQueryable result = options.ApplyTo(query, DefaultSettings);
由于我们在 DefaultSettings 中定义了页面大小,因此已经执行了数据库查询。定义页面大小会导致 ApplyTo 中的底层代码从数据库中检索所有数据,然后将检索到的列表作为可查询列表返回。这意味着在同步函数中查询数据库。
所以,我的问题是:有没有办法在不放弃异步读取的情况下实现对 OData 的分页?还是我在尝试这样做时过于复杂了?
【问题讨论】:
你能提供一些代码吗?根据您的解释,我认为在查询末尾添加.AsQueryable()
可能会解决问题
不确定您是要解决IQueryable .ToListAsync
问题还是要异步ODataQueryOptions.ApplyTo
。
指定了我的问题并提供了我们的一些代码。可悲的是, AsQueryable() 不起作用。当 ApplyTo 返回时,数据已经从数据库中检索出来了。
我也被这个问题难住了。我认为症结在于,任何地方都没有可以利用的 IQueryableAsync 接口。 .ToListAsync() 是 EF 中的 IQueryable 扩展,我敢肯定 OData 的人不想耦合到 EF。如果只有一个他们可以共享的通用接口,比如 IQueryableAsync 或其他东西,这将允许提供者允许异步执行,并允许消费者调用它。事实上,我们的 Get 似乎必须同步,这很不幸,因为我们通常得到的不仅仅是 Save。
@AndriiLitvinov 在使用分页的时候肯定会枚举出来。当我自己查看源代码时,我就知道了这一点。我没有设法解决这个问题。解决它的唯一方法是编写我自己的 ApplyTo 实现,我不愿意花时间在上面。我们决定采用更简单的路线,简单地使用控制器上的 EnableQuery 属性,让 OData 完成它的工作。生成的 SQL 查询远非理想,但就这样吧。性能和可扩展性还不是我们关心的问题。
【参考方案1】:
可以在 OData 中实现分页而不放弃异步读取。分页基本上意味着对 IQueryable 实例再应用一个表达式。调用 IQueryable.Take 后,可以调用 IQueryable.ToListAsync(枚举实际发生的地方)。
但是 Microsoft Web Api OData v4 实现使得查询枚举同步发生。见here。 ODataQuerySettings.ApplyTo 方法在内部使用 TruncatedCollection。 TruncatedCollection 继承自 System.Collections.Generic.List 并在创建时将 IQueryable 构造函数参数传递给 List 的 constructor,后者接受 IEnumerable,对其进行迭代并复制到内部数组中。
因此,您可以分叉 Web Api OData(因为它是开源的),对其进行调整并使其异步。或者实现您自己的 ODataQuerySettings.ApplyTo 版本,这将是异步的。
【讨论】:
是的,我提到 TruncatedCollection 是我的问题中的问题。但是,我尝试分叉,这会导致一些问题。这是一年多以前的事了,所以我记不太清了,但我知道这与一些在使用截断集合应用于分页之后应用的 OData 操作有关。特别是 Expand 选项引起了很多问题,这导致我必须编写自己的访问者模式并跳过其他循环才能完成所有这些工作。我决定不值得。当可扩展性成为更多问题时,我会重新考虑。【参考方案2】:我不知道你为什么要打电话给ToListAsync()
。应该没有必要。在您的操作方法中,您应该编写查询,但实际上并不获取任何数据。所有这些都应该是微不足道的,不需要异步。 IQueryable
稍后由框架执行。 (框架还应自动将查询字符串中的所有 OData 过滤器参数应用于您返回的查询包括分页)。
事实上,拥有IQueryable
(或IEnumerable
)类型的异步结果是没有意义的(see this answer)。不能异步枚举。
理论上,您可以将所有结果异步放入一个数组中,但是 Odata 会将其过滤器在内存中应用,而不是针对查询。我不知道你为什么需要这样做,但是使用 Odata 就没有什么意义了。
【讨论】:
重点不是我想要使用ToListAsync()
,而是异步执行数据库查询的一种方式。我不想阻塞数据库查询。如果没有分页,ToListAsync()
就可以了。据我所知,这样称呼并没有真正的缺点。当然,返回 IQueryable
也可以,因为 OData 处理可查询的解析。但是,我只想强制读取是异步的。
您可能从我的问题中误解了一件事情:“但是 Odata 会在内存中应用其过滤器,而不是针对查询”。 ApplyTo
将在执行查询之前应用它的过滤器,所以这不是真的。
我说“理论上你可以将所有结果异步放入一个数组中,但是 Odata 会在内存中应用它的过滤器”。如果你返回一个列表或数组,它绝对会。您需要让框架负责执行查询 - 它足够聪明,可以有效地将查询流式传输到响应并异步执行它们。
我认为你不明白 ODataQueryOptions.ApplyTo
做了什么。这会将所有 odata 选项(过滤、排序、扩展、选择等)应用到我提供的 IQueryable
。只有之后,我才会在从ODataQueryOptions.ApplyTo
返回的IQueryable
上调用ToListAsync
。因此,IQueryable
已经应用了所有 OData 选项。在调用 ApplyTo
之后调用 ToListAsync 会很好,如果它可以工作的话......它不会。
回想起来,这是迄今为止正确的答案,您根本不应该尝试使用 Async 来执行此操作。在 DbContext 等待时,您不能将其用于其他任何事情,因此它必须始终阻塞。所以 OP 一开始确实是在问错误的问题,他们应该做的是覆盖EnableQueryAttribute
以避免TruncatedCollection
问题或使用来自客户端的分页作为$top
和$skip
。以上是关于WebAPI OData v4 查询不是异步的?的主要内容,如果未能解决你的问题,请参考以下文章