如何在EF Core 3.1中以异步方式使用GroupBy?

时间:2020-01-29 11:00:58

标签: c# asp.net-core entity-framework-core ef-core-3.1

当我将GroupBy用作对EFCore的LINQ查询的一部分时,出现错误System.InvalidOperationException: Client-side GroupBy is not supported

这是因为EF Core 3.1试图尽可能在服务器端评估查询,而不是在客户端评估查询,并且该调用无法转换为SQL。

因此,以下语句不起作用,并产生上述错误:

var blogs = await context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"))
    .GroupBy(t => t.BlobNumber)
    .Select(b => b)
    .ToListAsync();

现在显然解决方案是在调用GroupBy()之前使用.AsEnumerable()或.ToList(),因为它明确告诉EF Core您要进行客户端分组。关于此on GitHubin the Microsoft docs的讨论。

var blogs = context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"))
    .AsEnumerable()
    .GroupBy(t => t.BlobNumber)
    .Select(b => b)
    .ToList();

但是,这不是异步的。如何使它异步?

如果我将AsEnumerable()更改为AsAsyncEnumerable(),则会收到错误消息。如果我改为尝试将AsEnumerable()更改为ToListAsync(),则GroupBy()命令将失败。

我正在考虑将其包装在Task.FromResult中,但这实际上是异步的吗?还是数据库查询仍然是同步的,只有随后的分组是异步的?

var blogs = await Task.FromResult(context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"))
    .AsEnumerable()
    .GroupBy(t => t.BlobNumber)
    .Select(b => b)
    .ToList());

或者如果那不起作用,还有另一种方法吗?

2 个答案:

答案 0 :(得分:5)

我认为您唯一的办法就是做这样的事情

var blogs = await context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"))
    .ToListAsync();

var groupedBlogs = blogs.GroupBy(t => t.BlobNumber).Select(b => b).ToList();

因为无论如何,GroupBy都会在客户端进行评估

答案 1 :(得分:3)

此查询未尝试从SQL / EF Core的角度对数据进行分组。没有涉及聚合。

它正在加载所有详细信息行,然后将其分批处理到客户端上的不同存储桶中。 EF Core不参与其中,这是一个纯粹的客户端操作。等效为:

var blogs = await context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"))
    .ToListAsync();

var blogsByNum = blogs.ToLookup(t => t.BlobNumber);

加快分组速度

批处理/分组/查找操作完全是受CPU约束的,因此加速它的唯一方法是对其进行并行化,即使用所有CPU对数据进行分组,例如:

var blogsByNum = blogs.AsParallel()
                      .ToLookup(t => t.BlobNumber);

ToLookupGroupBy().ToList()做更多或更少-它根据键将行分组到存储桶中

加载时分组

另一种方法是异步加载结果并将其放入存储桶中。为此,我们需要AsAsyncEnumerable()ToListAsync()一次返回所有结果,因此无法使用。

这种方法与ToLookup的方法非常相似。


var blogs = await context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"));

var blogsByNum=new Dictionary<string,List<Blog>>();

await foreach(var blog in blogs.AsAsyncEnumerable())
{
    if(blogsByNum.TryGetValue(blog.BlobNumber,out var blogList))
    {
        blogList.Add(blog);
    }
    else
    {
        blogsByNum[blog.BlobNumber=new List<Blog>(100){blog};
    }
}

通过调用AsAsyncEnumerable()执行查询。结果虽然异步到达,所以现在我们可以在迭代时将它们添加到存储桶中。

在列表构造函数中使用capacity参数,以避免重新分配列表的内部缓冲区。

使用System.LINQ.Async

如果我们对IAsyncEnumerable <>本身进行LINQ操作,事情会容易得多。 This extension名称空间提供了这一点。它是由ReactiveX团队开发的。可通过NuGet获得,当前的主要版本是4.0。

有了这个,我们可以这样写:

var blogs = await context.Blogs
    .Where(blog => blog.Url.Contains("dotnet"));

var blogsByNum=await blogs.AsAsyncEnumerable()   individual rows asynchronously
                          .ToLookupAsync(blog=>blog.BlobNumber);

var blogsByNum=await blogs.AsAsyncEnumerable()   
                          .GroupBy(blog=>blog.BlobNumber)
                          .Select(b=>b)
                          .ToListAsync();