【问题标题】:How to use GroupBy in an asynchronous manner in EF Core 3.1?如何在 EF Core 3.1 中以异步方式使用 GroupBy?
【发布时间】:2020-05-14 20:46:48
【问题描述】:

当我使用 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());

或者如果这不起作用,还有其他方法吗?

【问题讨论】:

  • 即使不存在,问题似乎仍然存在。 (上面的例子只是我从别处复制和粘贴的一个例子。我的实际代码不使用那个函数。)
  • 好的,你应该明确提到这一点。我会删除我的评论。那么。
  • 不管怎样,我认为如果使用 AsEnumerable 可以如此轻松地解决问题,那么必须有另一种简单的方法来做到这一点,但异步。我只是不知道它是什么。
  • 我已经简化了示例以去掉该功能。
  • @PanagiotisKanavos 同样,示例查询中具体的Select 对于所提出的问题(即 "How to use GroupBy in an )并不重要>EF Core 3.1 中的>异步方式?”

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


【解决方案1】:

我认为你唯一的方法就是这样做

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

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

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

【讨论】:

  • 这就是我最终的结果。它很简单,而且很有效。
【解决方案2】:

此查询并未尝试对 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);

ToLookup 或多或少与GroupBy().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();

【讨论】:

  • 为什么要重新打开以发布重复的答案(AsAsyncEnumerable() + System.LINQ.Async 包)?这个答案的第一部分在这种情况下没有意义。问题是(以及整个 GitHub 辩论)在 EF Core 查询中强制异步客户端评估的模式是什么。对于同步 LINQ,它很简单 AsEnumerable(),其余的通过 Enumerable LINQ。
  • 感谢您的信息。我考虑过使用 System.LINQ.Async,但最终还是选择了 Denis 的建议,因为它需要较少的依​​赖,并且适用于我的简单用例。不过会记住这个包以备将来使用。
猜你喜欢
  • 2021-09-05
  • 2021-09-19
  • 2021-07-30
  • 2018-03-07
  • 2020-07-09
  • 1970-01-01
  • 2021-01-21
  • 1970-01-01
  • 2020-06-22
相关资源
最近更新 更多