抛出异常使用流利的 api 从组中获取项目

Exception thrown Getting items from group using fluent api

这是我要完成的查询:

var contacts = await dbContext.Contacts
                .GroupBy(o => o.UserId)
                .Select(group => new
                {
                    UserId = group.Key,
                    Contacts = group.ToList()
                }).ToListAsync();

这是联系人实体:

[Table("Contacts")]
public class WAContact
{
    public int Id { get; set; }
    public string Phone { get; set; }
    public string Name { get; set; }
        
    [NotNull]
    public int UserId { get; set; }
    
    public WAUser User { get; set; }
    
}

此代码抛出此异常:

.ToList()' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().

我看过执行 ToList() 检索组项目的示例,但不知道我的代码中发生了什么。

P.D。经过更多测试后,我注意到我在调用 First()、Last() 等时也遇到了同样的错误。但是 Count() 例如可以工作。奇怪!

这是个问题。 联系人 = group.ToList()

为什么不像

那样更改代码
var grouprs= await dbContext.Contacts.Select(c=>c)
                .GroupBy(o => o.UserId);
           

您收到此异常是因为 EF 无法将 LINQ 转换为等效的 SQL。

将您的查询更改为此

// Load the Contact from the DB
var contacts = await dbContext.Contacts.ToListAsync();
// Perform the group by in memory
var userContacts = contacts
.GroupBy(o => o.UserId)
.Select(group => new
{
    UserId = group.Key,
    Contacts = group.Select(contact => new 
    {
      contact.Name,
      contact.Phone,
      contact.Id
    }).ToList()
}).ToList();

现在 EF 将能够将 LINQ 转换为正确的 SQL。

此查询无法翻译成 SQL。 我已经为此类错误写了一个小答案,您的查询位于列表的顶部:LINQ to Database: how to group entities properly and GroupBy limitations

您可以通过多种方式实现您想要的查询,具体取决于您想要的结果:

A) Return 所有 WAContact 实体

因为每个实体都必须有一个 UserId,所以不需要实际查询 WAUsers table:

var userIdsWithContactsWithoutJoin = context.Contacts
    .AsEnumerable()
    .GroupBy(c => c.UserId)
    .ToList();

代码仅执行 SELECT,然后切换到 client-evaluation 以对内存中的 returned 数据进行分组:

SELECT `c`.`Id`, `c`.`Name`, `c`.`Phone`, `c`.`UserId`
FROM `Contacts` AS `c`

B) Return 所有 WAUser ID 仅与相关的 WAContact 实体(完整)

var userIdsWithContacts = context.Users
    .SelectMany(
        u => context.Contacts
            .Where(c => u.Id == c.UserId),
        (u, c) => new
        {
            c.UserId,
            Contact = c
        })
    .AsEnumerable()
    .GroupBy(j => j.UserId, j => j.Contact)
    .ToList();

代码首先执行 INNER JOIN,然后切换到 client-evaluation 以对内存中的 returned 数据进行分组:

SELECT `c`.`UserId`, `c`.`Id`, `c`.`Name`, `c`.`Phone`
FROM `Users` AS `u`
INNER JOIN `Contacts` AS `c` ON `u`.`Id` = `c`.`UserId`

C) Return 所有 WAUser 实体(完整),有或没有相关的 WAContact 实体(完整)

var usersWithOrWithoutContacts = context.Users
    .SelectMany(
        u => context.Contacts
            .Where(c => u.Id == c.UserId)
            .DefaultIfEmpty(),
        (u, c) => new
        {
            User = u,
            Contact = c
        })
    .AsEnumerable()
    .GroupBy(j => j.User, j => j.Contact)
    .ToList();

代码首先执行 LEFT JOIN,然后切换到 client-evaluation 以对内存中的 returned 数据进行分组:

SELECT `u`.`Id`, `u`.`Name`, `c`.`Id`, `c`.`Name`, `c`.`Phone`, `c`.`UserId`
FROM `Users` AS `u`
LEFT JOIN `Contacts` AS `c` ON `u`.`Id` = `c`.`UserId`

所有三个查询return尽可能少的数据然后使用AsEnumerable()切换到client-evaluation在内存中执行实际分组。


示例程序

这是一个完整的示例项目,演示了查询(包括检查):

using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;

namespace IssueConsoleTemplate
{
    [Table("Contacts")]
    public class WAContact
    {
        public int Id { get; set; }
        public string Phone { get; set; }
        public string Name { get; set; }
        
        [NotNull]
        public int UserId { get; set; }
    
        public WAUser User { get; set; }
    }

    [Table("Users")]
    public class WAUser
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    // 
    // DbContext:
    // 

    public class Context : DbContext
    {
        public DbSet<WAContact> Contacts { get; set; }
        public DbSet<WAUser> Users { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseMySql(
                    "server=127.0.0.1;port=3306;user=root;password=;database=So64391764",
                    b => b.ServerVersion("8.0.21-mysql")
                          .CharSetBehavior(CharSetBehavior.NeverAppend))
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<WAUser>()
                .HasData(
                    new WAUser {Id = 1, Name = "John"},
                    new WAUser {Id = 2, Name = "Jane"},
                    new WAUser {Id = 3, Name = "Mike"});

            modelBuilder.Entity<WAContact>()
                .HasData(
                    new WAContact {Id = 11, Name = "John's First Contact", Phone = "12345", UserId = 1},
                    new WAContact {Id = 12, Name = "John's Second Contact", Phone = "23456", UserId = 1},
                    new WAContact {Id = 21, Name = "Jane's Only Contact", Phone = "09876", UserId = 2});
        }
    }

    internal class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
            
            // Return all WAContact entities. Because every entity must have a UserId,
            // there is no need to actually query the WAUsers table.
            // Just performs a SELECT, then switches to client-evaluation to group the returned
            // data in memory:
            //     SELECT `c`.`Id`, `c`.`Name`, `c`.`Phone`, `c`.`UserId`
            //     FROM `Contacts` AS `c`

            var userIdsWithContactsWithoutJoin = context.Contacts
                .AsEnumerable()
                .GroupBy(c => c.UserId)
                .ToList();

            Debug.Assert(userIdsWithContactsWithoutJoin.Count == 2);
            Debug.Assert(userIdsWithContactsWithoutJoin[0].Key == 1);
            Debug.Assert(userIdsWithContactsWithoutJoin[0].Count() == 2);
            Debug.Assert(userIdsWithContactsWithoutJoin[0].First().Name == "John's First Contact");

            // Return all WAUser Ids only with related WAContact entities (full).
            // First performs an INNER JOIN, then switches to client-evaluation to group the
            // returned data in memory:
            //     SELECT `c`.`UserId`, `c`.`Id`, `c`.`Name`, `c`.`Phone`
            //     FROM `Users` AS `u`
            //     INNER JOIN `Contacts` AS `c` ON `u`.`Id` = `c`.`UserId`

            var userIdsWithContacts = context.Users
                .SelectMany(
                    u => context.Contacts
                        .Where(c => u.Id == c.UserId),
                    (u, c) => new
                    {
                        c.UserId,
                        Contact = c
                    })
                .AsEnumerable()
                .GroupBy(j => j.UserId, j => j.Contact)
                .ToList();

            Debug.Assert(userIdsWithContacts.Count == 2);
            Debug.Assert(userIdsWithContacts[0].Key == 1);
            Debug.Assert(userIdsWithContacts[0].Count() == 2);
            Debug.Assert(userIdsWithContacts[0].First().Name == "John's First Contact");

            // Return all WAUser entities (full) with or without related WAContact entities (full).
            // First performs a LEFT JOIN, then switches to client-evaluation to group the returned
            // data in memory:
            //     SELECT `u`.`Id`, `u`.`Name`, `c`.`Id`, `c`.`Name`, `c`.`Phone`, `c`.`UserId`
            //     FROM `Users` AS `u`
            //     LEFT JOIN `Contacts` AS `c` ON `u`.`Id` = `c`.`UserId`

            var usersWithOrWithoutContacts = context.Users
                .SelectMany(
                    u => context.Contacts
                        .Where(c => u.Id == c.UserId)
                        .DefaultIfEmpty(),
                    (u, c) => new
                    {
                        User = u,
                        Contact = c
                    })
                .AsEnumerable()
                .GroupBy(j => j.User, j => j.Contact)
                .ToList();

            Debug.Assert(usersWithOrWithoutContacts.Count == 3);
            Debug.Assert(usersWithOrWithoutContacts[0].Key.Name == "John");
            Debug.Assert(usersWithOrWithoutContacts[0].Count() == 2);
            Debug.Assert(usersWithOrWithoutContacts[0].First().Name == "John's First Contact");
        }
    }
}

您也可以 运行 这个 .NET Fiddle(但使用 SQL 服务器而不是 MySQL)。


更多信息

有关 GROUP BY 查询的一般信息,请查看 Complex Query Operators