将 EF Core 查询从 2.2 转换为 3.0 - async await

Converting EF Core queries from 2.2 to 3.0 - async await

在 EF Core 2.2 中我有:

      var data = await _ArticleTranslationRepository.DbSet
        .Include(arttrans => arttrans.Article)
        .ThenInclude(art => art.Category)
        .Where(trans => trans.Article != null && trans.Article.Category != null && trans.Article.Category.Id == categoryId.Value)
        .GroupBy(trans => trans.ArticleId)
        .Select(g => new { ArticleId = g.Key, TransInPreferredLang = g.OrderByDescending(trans => trans.LanguageId == lang).ThenByDescending(trans => trans.LanguageId == defaultSiteLanguage).ThenBy(trans => trans.LanguageId).FirstOrDefault() })
        .Select(at => at.TransInPreferredLang)
        .OrderBy(at => at.Article.SortIndex)
        .ToListAsync();

现在使用 EF Core 3.0 我必须写:

      var data = _ArticleTranslationRepository.DbSet
  .Include(arttrans => arttrans.Article)
  .ThenInclude(art => art.Category)
  .Where(trans => trans.Article != null && trans.Article.Category != null && trans.Article.Category.Id == categoryId.Value)
    .AsEnumerable() // client side groupby is not supported (.net core 3.0 (18 nov. 2019)
  .GroupBy(trans => trans.ArticleId)
  .Select(g => new { ArticleId = g.Key, TransInPreferredLang = g.OrderByDescending(trans => trans.LanguageId == lang).ThenByDescending(trans => trans.LanguageId == defaultSiteLanguage).ThenBy(trans => trans.LanguageId).FirstOrDefault() })
  .Select(at => at.TransInPreferredLang)
  .OrderBy(at => at.Article.SortIndex)
  .ToList();

我的 asp.net 核心 mvc 动作方法是异步的 (public virtual async Task<ICollection<…>>…) 因为我使用 .AsEnumerable 来强制客户端评估,所以我还必须将 .ToListAsync() 更改为 .ToList() 并删除 await 运算符。

查询正常但产生警告: This async method lacs 'await' operators and will run synchronously. Consider using the 'await operator ….

如何重写此 EF Core 3.0 查询以使其使用异步/等待。我不知道如何在 单个 query/linq 表达式中包含 AsAsyncEnumerable()

(我知道我可以将它分成 'server' 部分和 'client-side' 部分,但我希望在单个 async[=32 中看到它=] 我之前在 EF Core 2.2 中使用的 linq 表达式。)

这个想法似乎是将 AsAsyncEnumerable()System.Linq.Async 包结合起来,后者为 IAsyncEnumerable<T>.

提供等效的 LINQ (IEnumerable<T>) 扩展方法

所以根据想法,如果您安装(或包参考)那个包,在 .GroupBy 之前插入 .AsAsyncEnumerable(),原始查询应该有效。

尽管 EF Core 3.0 DbSet<T> class 存在一个烦人的问题。由于它实现了 IQueryable<T>IAsyncEnumerable<T> 接口,并且其中 none 是 "better" (更接近)匹配,许多在 DbSet<T> 上使用标准 LINQ 运算符的查询将简单地在编译时使用 CS0121(不明确的调用)中断,并且需要添加 .AsQueryable()。使用 Include / ThenInclude 等 EF Core 特定扩展的查询将起作用,因为它们已经解析为 IQueryable<T>.

正如一些人在评论中提到的那样,可以使用 (await [serverPart...].ToListAsync())[clientPart...] 来消除 System.Linq.Async 的需要和相关的编译时方法歧义,但它与使用 [=26 具有相同的缺点=] 而不是同步场景中的 AsEnumerable(),方法是创建一个不必要的内存列表。


当然,最好的办法是通过找到等效但完全可翻译的 LINQ 构造来完全避免客户端评估。目前 GroupBy 很难,有时甚至是不可能的。即使有可能,它也需要通过删除 GroupBy 来重写以前的查询。例如,不是从 ArticleTranslation 开始查询并按 ArticleId 分组,而是从 Article 开始查询并使用 Translations 集合导航 属性 OrderByDescending()...FirstOrDefault() 支持。对每个失败的查询重复该过程。这样做的好处是,现在您的查询将首先执行服务器端。

我在 dotnet core 5.0 和 6.0 上遇到了同样的问题,我是这样解决的:

根据您的 dotnet 版本安装 nuget 包 System.Linq.Async https://www.nuget.org/packages/System.Linq.Async

然后在您的查询中您将能够添加 .ToAsyncEnumerable()

这是一个例子:

        public async Task<IEnumerable<ProductTableView>> GetProduct(Pagination pagination, string identifier)
    {

        var ListProductsTable = await _context.ModelView
                                       .FromSqlRaw("GetProductByIdentifier {0}", identifier)
                                       .ToAsyncEnumerable()
                                       .Select(a => new ProductTableView
                                       {
                                           ID = a.ID,
                                           Guid = a.Guid,
                                           Guid_Product_Category = a.Guid_Product_Category,
                                           Guid_Currency = a.Guid_Currency,
                                           NameProduct = a.NameProduct,
                                       }).ToListAsync();

        return ListProductsTable;
    }