Entity Framework 嵌套集合与另一个集合匹配的核心查询 returns 错误结果

Entity Framework Core query where nested collection matches another collection returns wrong results

我在使用 Entity Framework Core 5.x 时遇到问题,在尝试查找导航 属性 集合包含所有内容的实体集合时,会生成“错误”查询来自另一个列表的元素。

给定以下简化(但标准的多对多)模型

public class Announcement {
    public string Id { get; set; }
    public string Title { get; set; }
    public virtual IEnumerable<AnnouncementTag> Tags { get; set; }
}
    
public class Tag {
    public string Id { get; set; }
    public bool IsEnabled { get; set; } 
    public virtual IEnumerable<AnnouncementTag> Announcements{ get; set; }
}

// Needed since EF Core does not support many-to-many without explicit JOIN class
public class AnnouncementTag {
    public string AnnouncementId { get; set; }
    public string TagId { get; set; }
    public Announcement Announcement { get; set; }
    public Tag Tag { get; set; }
}

我想获取 所有标签 与给定列表中的内容匹配的公告列表。

例如用下面的数据

Announcement 1    has Tags A, B
Announcement 2    has Tags B
Announcement 3    has Tags C

查询“带有标签 ["B"] 的所有公告”将 return 公告 2,因为公告 3 没有任何匹配项,而公告 1 仅在一个标签上匹配。

我本以为下面的 LINQ 查询 return 我想要的结果

var applicableTags = new [] { "B" };

var results = db.Announcements.Where(a => a.Tags.All(at => applicableTags.Contains(at.Tag.Id));

但这会导致所有公告返回。只是为了简化查询,我修改为直接use JOIN class (TagId = Tag.Id)

var results = db.Announcements.Where(a => a.Tags.All(at => applicableTags.Contains(at.TagId));

但这会导致所有公告都被 return 编辑。生成的查询看起来很奇怪

SELECT * FROM [Announcements] AS [a]
WHERE NOT EXISTS (
    SELECT 1
    FROM [AnnouncementTag] AS [a1]
    INNER JOIN [Tags] AS [t1] ON [a1].[TagId] = [t1].[Id]
    -- Really... <>... opposite of what I would have expected
    -- When I query for more than one Tag...say A,B...the <> becomes "NOT IN"
    WHERE ([a].[Id] = [a1].[AnnouncementId]) AND ([t1].[Id] <> N'B')
)

我能够用 LINQ-to-Objects 复制这种行为,所以它 而不是 似乎是 Entity Framework 的事情...只是我不明白如何生成查询。

我确实找到了一篇描述以下工作的文章

// Notice how the applicableTags and Tags criteria is flipped from previous query
var results = db.Announcements.Where(a => applicableTags.All(at => a.Tags.Any(t => t.Id == at)));

确实如此……但仅适用于 LINQ-to-Objects。对于 Entity Framework,您会得到以下异常

System.InvalidOperationException: The LINQ expression 'DbSet<Announcement>()
  .Where(a => __applicableTags_0
      .All(at => DbSet<AnnouncementTag>()
          .Where(a0 => EF.Property<string>(a, "Id") != null && object.Equals(
              objA: EF.Property<string>(a, "Id"),
              objB: EF.Property<string>(a0, "AnnouncementId")))
          .Any(a0 => a0.TagId == at)))' 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 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'

但我不想为了在服务器上进一步过滤而不得不带回 所有 公告。

对我做错了什么有什么建议吗?有人可以解释一下我的原始查询中的缺陷,即 return 出乎意料的结果吗?

假设标签是唯一的,您可以通过以下方式表达您的查询:

var tagCount = applicableTags.Length;

var results = db.Announcements
    .Where(a => a.Tags.Where(t => applicableTags.Contains(t.Id)).Count() >= tagCount);