我应该从存储库返回Task<IEnumerable<T>>还是IAsyncEnumerable<T>?

13

在EF Core中实现仓库的正确方式是什么?

public IAsyncEnumerable<Order> GetOrder(int orderId)
{
    return blablabla.AsAsyncEnumerable();
}

或者

public Task<IEnumerable<Order>> GetOrder(int orderId)
{
    return blablabla.ToListAsync();
}

在性能方面,调用AsAsyncEnumerable()是否明智?这种方法安全吗? 一方面,它不会创建List<T>对象,因此应该稍微快一些。但另一方面,查询没有实现,所以我们推迟了SQL的执行,结果可能会在此期间发生变化。


请见 https://medium.com/@ben.k.muller/c-ienumerable-vs-list-and-array-9f099f157f4f。 - Henrique Mauri
1
这篇文章没有回答我的任何问题。 - Jan Kowalski
2个回答

7
根据来源.ToListAsync 内部无论如何都会使用 IAsyncEnumerable。因此,在一种或另一种方法上并没有太多性能上的好处。 但是,.ToListAsync 或者 .ToArrayAsync 的一个重要特点是取消
public static async Task<List<TSource>> ToListAsync<TSource>(
    this IQueryable<TSource> source,
    CancellationToken cancellationToken = default)
{
    var list = new List<TSource>();
    await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken))
    {
        list.Add(element);
    }
    return list;
}

List会在内存中存储所有内容,但如果列表非常大,则可能会严重影响性能。在这种情况下,您可以考虑对大型响应进行分页处理。

public Task<List<Order>> GetOrders(int orderId, int offset, int limit)
{
    return blablabla.Skip(offset).Take(limit).ToListAsync();
}

如果你要查找的项是第一项,使用asyncEnumerableList.firstOrDefaultAsync()比list.firstOrDefault()有性能上的优助吗? - Burak Kalafat

6
这个决定实际上取决于你想要缓存还是流式处理结果。
如果你想缓存结果,请使用ToList()或者ToListAsync()。如果你想流式处理结果,请使用AsEnumerable()或者AsAsyncEnumerable()
根据文档的解释:

缓存意味着将查询结果全部加载到内存中,而流处理则表示EF每次只向应用程序传递单个结果,而不会在内存中包含全部结果集。原则上,流式查询的内存需求是固定的 - 无论查询返回1行还是1000行,它们都是相同的;而缓存查询则需要更多内存来存储返回的行数越多。对于返回大型结果集的查询,这可能是一个重要的性能因素。

通常情况下,最好选择流式处理,除非必须缓存。
当你流式处理时,一旦数据被读取,你就不能再次读取而不重新访问数据库。所以如果你需要多次读取同一数据,你需要进行缓存。
如果一个仓库通过IEnumerable进行流式处理,调用者可以通过调用ToList()(或者在IAsyncEnumerable上调用ToListAsync())来选择缓存。如果仓库选择返回IList,我们将失去这种灵活性。
因此,回答你的问题,最好是让仓库对结果进行流式处理,然后让调用者决定是否要进行缓存。
如果项目团队对流式处理语义不太熟悉,或者大部分代码已经进行了缓存,那么为流式处理的方法添加一个像AsStream这样的后缀(例如GetOrdersAsStream())可能是有意义的,以便他们知道不应该枚举它超过一次。
所以一个仓库可以有:
async Task<List<Order>> GetOrders() => await GetOrdersAsStream.ToListAsync();
IAsyncEnumerable<Order> GetOrdersAsStream() => ...

AsEnumerable() 延迟执行,直到开始枚举结果。因此,第一个示例中的方法(采用 IAsyncEnumerable 返回类型)不会在存储库内部实现查询。 - Jan Kowalski
@JanKowalski 我明白了,我已经更新了答案。无论是否在仓库内部命中数据库,只要我们知道数据是否正在被流式传输(并且从而会在每个枚举时导致一次数据库查询)或者它是缓冲的,都没有关系。 - galdin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接