我有三门课:
public abstract class Conversation
{
public int Id { get; set; }
public abstract bool HasUser(string userId);
}
public class PublicConversation : Conversation
{
public override bool HasUser(string userId)
{
return true;
}
}
public class PrivateConversation : Conversation
{
public ICollection<User> Users { get; set; }
public override bool HasUser(string userId)
{
return Users.Any(t => t.UserId == userId);
}
}
在DbContext中,有DbSet:
public DbSet<Conversation> Conversations { get; set; }
public DbSet<PrivateConversation> PrivateConversations { get; set; }
public DbSet<PublicConversation> PublicConversations { get; set; }
public DbSet<User> Users { get; set; }
生成两个表:Conversations
和 Users
。
PrivateConversations and PublicConversations are saved in table
Conversations
现在查询发生错误:
// For some reason, I can only query `db.Conversations`, can not access `db.PrivateConversations`
var conversations = db.Conversations
.Include(t => (t as PrivateConversation).Users)
.Where(t => t.HasUser("something"))
.ToList();
对于所有PublicConversation
,一切正常。
告诉我,在 PrivateConversation
方法中:HasUser()
、Users.Any()
、Users
是空。
我很困惑。如何查询其中与用户的所有私有(private)对话?
其他信息:
我使用的包:
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SQLServer" Version="2.2.6" />
运行时:.NET Core 2.2
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
最佳答案
表达式
t.HasUser("something")
无法转换为 SQL,因为它需要 Conversation
对象 和调用 HasUser
方法。只有不需要物化对象的众所周知的方法才可以转换为SQL。
查询翻译器以不同的方式处理不可翻译的表达式。 EF6 抛出异常。 EF Core 3.0+ 中也是如此。 。但是您似乎正在使用的 EF Core 1.x/2.x 尝试评估此类表达式 client side .
问题是,如果条件使用实体基元属性,它会起作用,但导航属性会失败,因为在客户端评估时,它们尚未加载,即使稍后将使用 Include
加载它们>。您可以通过初始化集合导航属性或添加 null
检查来避免 NRE,但无论哪种情况,结果都不会正确。
这一点,再加上隐藏的低效率,是在 3.0 中删除隐式客户端评估的原因之一。
有两种解决方案:
(1) 保持封装并使用显式客户端评估。显式客户端评估意味着您在查询中的某个点显式插入 AsEnumerable()
。在此之前的所有内容都将由 EF Core 执行,之后的所有内容都将由 LINQ to Objects 在 LINQ to Entities (EF Core) 查询的完全具体化结果上执行。
var conversations = db.Conversations
.Include(t => (t as PrivateConversation).Users)
.AsEnumerable() // <--
.Where(t => t.HasUser("something"))
.ToList();
(2) 打破封装并使用可翻译结构重新创建表达式。这样过滤将发生在服务器端:
var conversations = db.Conversations
.Include(t => (t as PrivateConversation).Users)
.Where(t => t is PrivateConversation ?
((PrivateConversation)t).Users.Any(u => u.Name == "something") :
true)
.ToList();
即而不是
t.HasUser("something")
你会使用
t is PrivateConversation ?
((PrivateConversation)t).Users.Any(u => u.Name == "something") :
true
或同等内容
!(t is PrivateConversation)
|| ((PrivateConversation)t).Users.Any(u => u.Name == "something")
它从 HasUser
方法的派生类重写中提取内联逻辑。
请注意,EF Core(或任何其他库)无法“查看”类似于 C# 编译器的方法的实现,因为它没有源代码(除非它尝试反编译已编译的代码,这不是一个简单的任务) .
两种解决方案都有优点和缺点。从 OOP 的角度来看,(1) 更好,但效率较低,因为加载的数据(和相关数据)可能比其需要的多得多。 (2) 是相反的 - 性能/内存使用更好,OOP 更糟糕(破坏封装,需要更新,以防相关方法的新派生类实现等),因此请使用更适合您需求的那个。
关于entity-framework-core - Entity Framework - 使用派生类型方法有条件地查询相关实体,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57728370/