迁移到 .net core 3.1 后,EF 的 OrderBy 出现问题。

17

考虑以下代码:

_dbContext.Messages
    .GroupBy(m => new
        {
            MinId = m.SenderId <= m.RecipientId ? m.SenderId : m.RecipientId,
            MaxId = m.SenderId > m.RecipientId ? m.SenderId : m.RecipientId
        })
        .Select(gm => gm.OrderByDescending(m => m.SentAt).FirstOrDefault());

我通过用户Id将所有对话分组,无论是谁发送的消息。然后在组内按SentAt日期排序消息,并选择每个对话中的最后一条消息。这段代码曾经能够正常工作,并且将所有内容转换为纯T-Sql(我使用SQL Server Profiler进行了检查)。但是,当我决定将我的项目从Core 2.1迁移到3.1时,现在出现了以下问题:“LINQ表达式”(GroupByShaperExpression: KeySelector:”。{{请注意保留原文中的特殊符号和格式}}。
new { 
    MinId = (CASE
        WHEN ((m.SenderId) <= (m.RecipientId)) THEN (m.SenderId)
        ELSE (m.RecipientId)
    END), 
    MaxId = (CASE
        WHEN ((m.SenderId) > (m.RecipientId)) THEN (m.SenderId)
        ELSE (m.RecipientId)
    END)
 }, 
ElementSelector:(EntityShaperExpression: 
    EntityType: Message
    ValueBufferExpression: 
        (ProjectionBindingExpression: EmptyProjectionMember)
    IsNullable: False
)
).OrderByDescending(m => m.SentAt)

无法翻译。要么将查询重写为可翻译的形式,要么通过插入对AsEnumerable()、AsAsyncEnumerable()、ToList()或ToListAsync()的调用来明确切换到客户端评估。有关更多信息,请参见https://go.microsoft.com/fwlink/?linkid=2101038
如果有任何改进意见,我们将不胜感激。
附言:我知道我可以深入研究T-SQL并编写存储过程,但我仍然希望找到一种使用Linq to Entity实现它的方法。

2
如果您可以相信最近的消息也具有最高的MessageID,那么您可以获得每个MinId,MaxId对的Max(MessageID)并通过Contains查询(所有在一个表达式中)检索相关消息。这将产生比@Ivan展示的不可避免的解决方法更为合理的SQL。然而,这使代理键不舒服地接近于添加业务含义。 - Gert Arnold
@GertArnold 谢谢。但问题是 - 我的 MessageId 是 Guid 类型。 - Alex
1
呵呵,那个解决方案就这样了。 - Gert Arnold
1个回答

27

很遗憾,当前EF Core 3.0 / 3.1仅支持对键/聚合进行投影的GroupBy的服务器翻译(类似于SQL)。

这是不可接受的,因为尽管EF6也没有客户端评估,但它能够成功地翻译这样的查询。

GroupBy翻译问题得到解决之前,解决方法是用两个相关子查询替换GroupBy——第一个子查询只包含分组键,第二个子查询包含组元素。

在你的情况下,应该像这样:

var source = _dbContext.Messages
    .Select(m => new
    {
        Key = new
        {
            MinId = m.SenderId <= m.RecipientId ? m.SenderId : m.RecipientId,
            MaxId = m.SenderId > m.RecipientId ? m.SenderId : m.RecipientId
        },
        Message = m
    });

var query = source.Select(e => e.Key).Distinct()
    .SelectMany(key => source
        .Where(e => e.Key.MinId == key.MinId && e.Key.MaxId == key.MaxId)
        .Select(e => e.Message)
        .OrderByDescending(m => m.SentAt)
        .Take(1));

我被重定向到这里,来自我的一个问题。第一个查询不会检索客户端上的所有数据表吗?这可能会有影响。 - anon
2
@OlivierMATROT 不,这就是整个想法 - 上述技术创建了可由服务器翻译的查询。请注意,sourceIQueryable<T>(因此未被执行),并且在 GroupBy 之前包含公共查询部分(如果有过滤器)。 - Ivan Stoev
好的,我现在明白了。还在尝试理解你给出的解决方案来解决我的问题。 - anon
@OlivierMATROT 将其应用于您的情况将是这样的:var sourceQuery = context.CallbackHistoryDbSet .Where(e => e.CompanyId == companyId) .Select(e => new { Key = new { e.Caller.PhoneNumber }, Entry = e }); var query = sourceQuery .Select(e => e.Key).Distinct() .SelectMany(key => sourceQuery .Where(e => e.Key.PhoneNumber == key.PhoneNumber) .Select(e => e.Entry) .OrderByDescending(e => e.LastCallTimeStamp).Take(1)) .Include(e => e.Caller) .Take(5) .AsNoTracking(); - Ivan Stoev
好的。谢谢你。 - anon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接