【问题标题】:Convert non-async Linq Extension method to Async [closed]将非异步 Linq 扩展方法转换为异步 [关闭]
【发布时间】:2021-02-22 18:23:37
【问题描述】:

我的问题和这个一样: Scalable Contains method for LINQ against a SQL backend

概要:用户向我的 asp.net 异步控制器方法发布了一个 long(id)列表。控制器需要为每个 id 从 SQL 数据库中提取两列,并将其作为 json 数组返回。由于我使用的是 EF/Linq,如上面的链接中所述,因此我使用 Contains 方法:

long[] ids;   //Assume that the posted list of ids to the controller method

var courses = await db.Courses.AsNoTracking().Where(x => ids.Contains(x.id))
.Select(x => new JRecord { id = x.id, name = x.name, status = x.status})
.ToListAsync();

return Request.CreateResponse(HttpStatus.OK, courses);

EF 将 Contains 转换为 SQL IN 语句。问题是大多数时候,id 列表只有 100 个,这很好,但用户也可以选择几千个条目,这会导致查询非常慢或查询完全失败

作者(在上面的链接中)发布了针对另一个问题的解决方案,其中 Linq Extension 只是将 IDs 数组拆分为块,将每个块作为单独的 & 较小的查询运行,然后将所有查询的结果合并回一个列表. 原因不是为了提高性能,而是为了确保在提供大量 id 时查询不会失败。

他的代码:https://stackoverflow.com/a/6852288/934257

public static IEnumerable<IEnumerable<T>> ToChunks<T>(this IEnumerable<T> enumerable, int chunkSize)
{
     int itemsReturned = 0;
     var list = enumerable.ToList(); // Prevent multiple execution of IEnumerable.
     int count = list.Count;
     while (itemsReturned < count)
     {
          int currentChunkSize = Math.Min(chunkSize, count - itemsReturned);
          yield return list.GetRange(itemsReturned, currentChunkSize);
          itemsReturned += currentChunkSize;
     }
}

使用他的 Linq 扩展:

var courses = ids.ToChunks(1000)
                 .Select(chunk => Courses.Where(c => chunk.Contains(c.CourseID)))
                 .SelectMany(x => x).ToList();

我希望在我的场景中采用这个扩展,所以我可以使用一个简单的构造,例如 ToChunks(1000),它将 ID 数组拆分为 1000 个长度的部分,对每个 ID 部分运行异步 EF 查询并合并结果重新组合成一个列表。与手动拆分 ID 数组、创建 for 循环并在 ID 数组部分上单独运行查询并将结果合并回列表相比,这将是更清洁和可重用的解决方案。

【问题讨论】:

  • 这里讲异步操作是没有意义的,除非源本身是异步的;你打算切换到IAsyncEnumerable&lt;T&gt; 吗?如果是这样,nuget.org/packages/System.Linq.Async 可能会提供您需要的一切,但是:这是一个很大的变化。再次强调:你不能只是挥动魔杖,让非异步方法变为异步 - 你所做的事情需要固有地支持异步
  • @MarcGravell 在上面的示例中,ids 是 List,Courses 是表示 SQL Server 中表的 EF 模型。该方法会在 asp.net webapi async 控制器中调用,因此需要同时使数据检索异步。
  • 主要问题是我的端点将获得一个可能包含 4000 个 ID 的列表,我需要在数据库中查找。传统的 Linq Contains() 性能不够,而且这种非异步的 Extension 方法将 id 分成块并创建多个 sql 查询。然而,由于答案很老,它使用非异步语义编写,因此不是今天的最佳解决方案。
  • 什么是list.GetRange,它有异步版本吗?
  • 没有。这是一个使用IEnumerable的扩展方法,扩展方法我不擅长,但我的理解是Async版本使用IQueryable接口。

标签: c# entity-framework linq


【解决方案1】:

这是一个 XY 问题。您要达到的目标与您的问题所问的不同。即使您将此方法设为异步,您想要的语法也不起作用:

var courses = await ids.ToChunks(1000)
                 .Select(chunk => Courses.Where(c => chunk.Contains(c.CourseID)))
                 .SelectMany(x => x).ToListAsync();

而您真正想要的结果同样可以轻松实现,而无需将此方法设为异步:

var courses = new List<Course>();
foreach(var chunk in ids.ToChunks(1000))
{
    courses.AddRange(await Courses.Where(c => chunk.Contains(c.CourseID)).ToListAsync());
}

注意:作为 Syatoslav pointed out,任何通过并发获得更好性能的尝试都将因 DbContext 不允许您执行多个并发异步调用这一事实而受阻。所以无论如何,你真的不会从异步中获得太多优势。而in some cases,您可能会发现它要慢得多。

【讨论】:

  • 你引入了另一个问题。 DbContext 不是线程安全的。
  • 好点。我忘了!
  • 又错了Where 不是物化方法,也不是异步方法。
  • @SvyatoslavDanyliv:我仍在努力解决问题。现在怎么样?
  • @tunafish24:您可能仍然可以创建扩展方法来提供帮助:它们只是与您要求的不同。例如,在您链接的帖子中,有一个“ChunkyContains”方法:这是您可以在这种情况下进行异步并重用的方法。但是,除非您尝试利用取消令牌或其他东西,否则将其设为异步可能是浪费时间。只需使用您已经获得的同步代码即可。
【解决方案2】:

我能想象到的最简单的实现:

public static async Task<IEnumerable<IEnumerable<T>>> ToChunksAsync<T>(this IQueryable<T> enumerable, int chunkSize)
{
     int itemsReturned = 0;
     var list = await enumerable.ToListAsync(); 
     int count = list.Count;
     while (itemsReturned < count)
     {
          int currentChunkSize = Math.Min(chunkSize, count - itemsReturned);
          yield return list.GetRange(itemsReturned, currentChunkSize);
          itemsReturned += currentChunkSize;
     }
}

当然,我们可以使用 IAsyncEnumerable,但这是另一个挑战。目前我没有看到这样做的性能优势。

【讨论】:

  • 应该注意的是,与简单地调用await ....ToListAsync() 并将结果传递给原始.ToChunks() 方法相比,这种ToChunksAsync() 实现几乎没有任何优势。
  • @StriplingWarrior,同意。有时人们想要一些东西而不了解什么是异步以及编译器做了什么来使异步魔术起作用。
猜你喜欢
  • 1970-01-01
  • 2019-12-26
  • 1970-01-01
  • 2020-12-26
  • 2021-04-12
  • 1970-01-01
  • 2018-10-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多