ToAsyncEnumerable().Single() 与 SingleAsync()

ToAsyncEnumerable().Single() vs SingleAsync()

我正在以独立于 EF-Core 的方式构建和执行我的查询,因此我依赖 IQueryable<T> 来获得所需的抽象级别。我正在用等待的 ToAsyncEnumerable().Single() 呼叫替换等待的 SingleAsync() 呼叫。我还将 ToListAsync() 调用替换为 ToAsyncEnumerable().ToList() 调用。但我只是偶然发现了 ToAsyncEnumerable() 方法,所以我不确定我是否正确使用了它。

为了阐明我指的是哪些扩展方法,它们的定义如下:

当查询针对 EF-Core 运行时,调用 ToAsyncEnumerable().Single()/ToList()SingleAsync()/ToListAsync() 在功能和性能上是否相同?如果不是那么它们有何不同?

我看了一眼Single(第90行)的源代码。
它清楚地说明了枚举器只前进了一次(对于成功的操作)。

        using (var e = source.GetEnumerator())
        {
            if (!await e.MoveNext(cancellationToken)
                        .ConfigureAwait(false))
            {
                throw new InvalidOperationException(Strings.NO_ELEMENTS);
            }
            var result = e.Current;
            if (await e.MoveNext(cancellationToken)
                       .ConfigureAwait(false))
            {
                throw new InvalidOperationException(Strings.MORE_THAN_ONE_ELEMENT);
            }
            return result;
        }

由于这种实现方式(现在)已经很好了,所以可以肯定地说,使用 Ix 单运算符不会损害性能。

至于 SingleAsync,你可以确定它是以类似的方式实现的,即使不是(这是值得怀疑的),它也无法胜过 Ix Single 运算符。

对于返回序列的方法(如 ToListAsyncToArrayAsync)我不希望有什么不同。

但是对于单值返回方法(FirstFirstOrDefaultSingleMinMax、[=17= 的异步版本]等)肯定会有区别。这与在 IQueryable<T>IEnumerable<T> 上执行这些方法的区别相同。在前一种情况下,它们由数据库查询处理,将单个值返回给客户端,而在后一种情况下,整个结果集将返回给客户端并在内存中处理。

所以,虽然一般来说抽象 EF Core 的想法是好的,但它会导致 IQueryable<T> 的性能问题,因为可查询的异步处理没有标准化,并且转换为 IEnumerable<T> 会改变执行上下文,因此实现单值返回 LINQ 方法。

P.S。通过标准化,我的意思如下。 IQueryable的同步处理由IQueryProvider提供(standard interface from System.Linq namespace in System.Core.dll assembly)Execute方法。异步处理需要引入另一个类似于 EF Core custom IAsyncQueryProviderstandard 接口(在 Microsoft.EntityFrameworkCore.Query.Internal 命名空间内 Microsoft.EntityFrameworkCore.dll 组装)。我想这需要 BCL 团队 cooperation/approval 并且需要时间,这就是他们决定暂时采用自定义路径的原因。

当原始来源是 DbSet 时,在数据库包含多个匹配行的例外情况下,ToAsyncEnumerable().Single() 的性能不如 SingleAsync()。但在更可能的情况下,您都只期望并收到一行,这是一样的。比较生成的SQL:

SingleAsync():
    SELECT TOP(2) [l].[ID]
    FROM [Ls] AS [l]

ToAsyncEnumerable().Single():
    SELECT [l].[ID]
    FROM [Ls] AS [l]

ToAsyncEnumerable() 打破了 IQueryable 调用链并进入 LINQ-to-Objects 领域。任何下游过滤都发生在内存中。您可以通过在上游进行过滤来缓解这个问题。所以而不是:

ToAsyncEnumerable().Single( l => l.Something == "foo" ):
    SELECT [l].[ID], [l].[Something]
    FROM [Ls] AS [l]

你可以做到:

Where( l => l.Something == "foo" ).ToAsyncEnumerable().Single():
    SELECT [l].[ID], [l].[Something]
    FROM [Ls] AS [l]
    WHERE [l].[Something] = N'foo'

如果这种方法仍然让您感到局促不安,那么,作为替代方案,请考虑像这样定义扩展方法:

using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.Internal;

static class Extensions
{
    public static Task<T> SingleAsync<T>( this IQueryable<T> source ) =>
        source.Provider is IAsyncQueryProvider
            ? EntityFrameworkQueryableExtensions.SingleAsync( source )
            : Task.FromResult( source.Single() );
}

根据 EF Core 的 Microsoft 官方文档(所有版本,包括当前的 2.1 版本):

This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code. This API may change or be removed in future releases.

来源:https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.query.internal.asynclinqoperatorprovider.toasyncenumerable?view=efcore-2.1

p.s。我个人发现它与 AutoMapper 工具结合使用时存在问题(至少,直到版本 6.2.2) - 它只是不映射 IAsyncEnumerable 类型的集合(与 IEnumerable 不同,AutoMapper 可以无缝地使用它)。