如何为空导航过滤 IQueryable 属性

How to filter IQueryable for null navigation property

假设我有 2 个简单模型(如下),其中 'User' 包含导航 属性 到 'UserDetail'(一对一关系):

数据库

CREATE TABLE [dbo].[UserDetail]
(
    [UserId] [int] NOT NULL PRIMARY KEY,
    [Name] varchar(100) NULL
)
CREATE TABLE [dbo].[User]
(
    [UserId] [int] NOT NULL PRIMARY KEY
)

C#.NET

[Table("UserDetail"), Schema="dbo"]
public class UserDetail
{
    [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int UserId { get; set; }
    public string Name { get; set; }
    // Additional Properties Not Listed
}

[Table("User"), Schema="dbo"]
public class User
{
    [Key, Required]
    public int UserId { get; set; }
    // Additional Properties Not Listed

    [ForeignKey("UserId")]
    public virtual UserDetail UserDetail { get; set; }
}

现在,.NET 应用程序正在使用存储库体系结构,因此我最初为 'User' table(默认 Entity Framework 行为;延迟加载):

IQueryable<User> users = DataRepository.UserRepository.List(); // 'List()' returns an IQueryable

我在这里的最终目标是简单地添加一个过滤器,它只会拉取 'User'(s) 没有关联的 'UserDetail' 记录(即导航 属性 将是空):

users = users.Where(m => m.UserDetail == null); // Also tried (m => m.UserDetail.Equals(null))
var testing = users.ToList();

但是在检查 'testing' 变量时,查询总是 returns 为空(在数据库中,有 'User' 条关联和不关联 'UserDetails' 的记录)。根据一些初步研究,这似乎可能是由于延迟加载造成的。为了验证,我尝试了以下示例:

users = users.Where(m => m.UserDetail.Name == "UserName");
var testing = users.ToList();

令我惊讶的是,这按照最初的预期工作; 'testing' 变量包含 'User' 条记录,这些记录有一条关联的 'UserDetail' 条记录,其中 'Name' 属性 等于 'UserName'。现在看来延迟加载不是问题,而是 IQueryable 比较 NULL 导航属性的潜在问题(在 SQL 查询转换期间?)。

在最终测试中,决定尝试使用 Include 方法查看是否有所不同,但它产生的结果与第一个测试相同(即为空):

users = users.Include(m => m.UserDetail).Where(m => m.UserDetail == null);
var testing = users.ToList();

所以我现在不知所措,显然我理解得不够好,无法产生我正在寻找的结果。会喜欢任何知道我在这个问题上被绊倒的人的任何 advice/help。

我最好的猜测是 IQueryable 查询将导航属性转换为 'JOIN'(s) 而不是 'LEFT JOIN'(s),因此不能用于比较 empty/null 相关记录(记录根本不存在)。

重述问题:

  1. 我如何在 .NET/Entity 框架中编写将在数据库上执行的查询,以仅提取没有关联 'UserDetail' 记录的 'User'(s)?
  2. 为什么 IQueryable 没有提取我在第一次测试中期望的记录?
  3. 澄清我对 IQueryable 功能的错误陈述的任何一般要点

提前感谢任何help/advice!

最终,问题归结为 Entity Framework 的关系配置(数据注释)不正确。正在尝试配置 one-to-(一个或 none)关系,Entity Framework 将其解释为 one-to-一个(两者都是必需的)。因此,IQueryable/Include 函数正在执行 JOIN 而不是所需的 LEFT JOIN。

我参考了两篇文章,使我产生了这种想法 (this and this)

所需的代码更改是针对 C#.NET 模型:

[Table("UserDetail"), Schema="dbo"]
public class UserDetail
{
    // Other articles solution was to remove the 'Required' data annotation, but I found
    // it not to be necessary (especially if it's used for validation)
    [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.None)] 
    public int UserId { get; set; }
    public string Name { get; set; }
    // Additional Properties Not Listed

    // Required so that 'UserId' can be considered 'nullable';
    // not just a primary key (non-nullable)
    [ForeignKey("UserId")]
    // Needed to determine 'principle end' between 'User' and 'UserDetail' relationship
    [Required]
    public virtual UserDetail UserDetail { get; set; }
}

[Table("User"), Schema="dbo"]
public class User
{
    [Key, Required]
    public int UserId { get; set; }
    // Additional Properties Not Listed

    //[ForeignKey("UserId")] // Optional: not needed in my case but won't break anything
    public virtual UserDetail UserDetail { get; set; }
}

现在 IQueryable 查询的工作方式为 expected/intended:

IQueryable<User> users = DataRepository.UserRepository.List().Where(m => m.UserDetail == null);
// Now, it only displays 'User' records that don't have an associated 'UserDetail' record
// FYI, no 'Include()' method required
var testing = users.ToList();