Как использовать GroupBy в асинхронном режиме в EF Core 3.1? - PullRequest
3 голосов
/ 29 января 2020

Когда я использую GroupBy как часть запроса LINQ к EFCore, я получаю ошибку 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();

Теперь, очевидно, решение состоит в использовании .AsEnumerable () или .ToList () перед вызовом GroupBy ( ), поскольку это явно говорит EF Core, что вы хотите выполнить группировку на стороне клиента. Об этом есть обсуждение на GitHub и в документах Microsoft .

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

Однако это не асинхронно. Как я могу сделать это асинхронным?

Если я изменяю AsEnumerable () на AsAsyncEnumerable (), я получаю ошибку. Если я вместо этого попытаюсь изменить AsEnumerable () на ToListAsyn c (), то команда 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 ]

2 голосов
/ 29 января 2020

Этот запрос не пытается группировать данные в смысле SQL / EF Core. Там нет агрегации участвуют.

Он загружает все строки сведений, а затем группирует их в разные сегменты на клиенте. EF Core не участвует в этом, это чисто клиентская операция. Эквивалент будет выглядеть следующим образом:

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

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

Ускорение группировки

Операция пакетирования / группировки / поиска связана исключительно с процессором, поэтому единственный способ ускорить ее - чтобы распараллелить его, ie использует все процессоры для группировки данных, например:

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.Asyn c

Все было бы намного проще, если бы у нас были операции LINQ для самого IAsyncEnumerable <>. Это расширение namespace обеспечивает именно это. Он разработан командой 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();
1 голос
/ 29 января 2020

Я думаю, что единственный способ, который у вас есть, это просто сделать что-то вроде этого

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

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

Поскольку GroupBy все равно будет оцениваться на клиенте

...