全局查询筛选器

全局查询筛选器是应用于元数据模型中实体类型的 LINQ 查询谓词(通常位于 OnModelCreating)。 查询谓词是通常传递给 LINQ Where 查询运算符的布尔表达式。 EF Core 会自动将此类筛选器应用于涉及这些实体类型的任何 LINQ 查询。 EF Core 还将其应用于实体类型,通过使用 Include 或导航属性间接引用它们。 此功能的一些常见应用程序包括:

  • 软删除 - 实体类型定义属性 IsDeleted
  • 多租户 - 一个实体类型定义了TenantId属性。

示例:

以下示例演示如何使用全局查询筛选器在简单的博客模型中实现多租户和软删除查询行为。

小窍门

可以在 GitHub 上查看本文 的示例

注释

此处使用多租户作为一个简单的示例。 还有一篇文章提供了关于 EF Core 应用程序中实现多租户的综合指南。

首先,定义实体:

public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
    private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public Blog Blog { get; set; }
}

请注意在_tenantId实体上声明的Blog字段。 此字段将用于将每个博客实例与特定租户相关联。 还定义了 IsDeleted 属性在 Post 实体类型上。 此属性用于跟踪帖子实例是否已“软删除”。 也就是说,实例标记为已删除,无需以物理方式删除基础数据。

接下来,使用 OnModelCreating API 在 HasQueryFilter 中配置查询筛选器。

modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

传递给调用的 HasQueryFilter 谓词表达式现在将自动应用于这些类型的任何 LINQ 查询。

小窍门

请注意 DbContext 实例级别字段的使用: _tenantId 用于设置当前租户。 模型级筛选器将使用正确上下文实例(即正在执行查询的实例)中的值。

注释

目前无法在同一实体上定义多个查询筛选器 - 仅应用最后一个查询筛选器。 但是,可以使用逻辑 AND 运算符(&& 在 C# 中)定义具有多个条件的单个筛选器。

导航功能的使用

还可以在定义全局查询筛选器时使用导航。 在查询筛选器中使用导航会导致以递归方式应用查询筛选器。 当 EF Core 扩展查询筛选器中使用的导航时,它还将应用对引用实体定义的查询筛选器。

若要设置查询筛选器 OnModelCreating,请按以下方式进行:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));

接下来,查询所有 Blog 实体:

var filteredBlogs = await db.Blogs.ToListAsync();

此查询生成以下 SQL,该 SQL 应用到定义的 BlogPost 实体的查询筛选器:

SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Posts] AS [p]
    WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0

注释

目前 EF Core 不会检测全局查询筛选器定义中的周期,因此在定义它们时应小心。 如果指定不正确,则循环可能会导致查询转换期间出现无限循环。

使用所需的导航访问具有查询筛选器的实体

谨慎

使用必须的导航访问已定义全局查询筛选器的实体可能会导致意外结果。

必需的导航要求相关实体总是存在。 如果查询筛选器筛选掉了必要的相关实体,则父实体也不会出现在结果中。 因此,结果的元素数量可能少于预期。

为了说明问题,我们可以使用上面指定的 BlogPost 实体以及以下 OnModelCreating 方法:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

可以使用以下数据对模型进行种子设定:

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/fish",
        Posts = new List<Post>
        {
            new Post { Title = "Fish care 101" },
            new Post { Title = "Caring for tropical fish" },
            new Post { Title = "Types of ornamental fish" }
        }
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts = new List<Post>
        {
            new Post { Title = "Cat care 101" },
            new Post { Title = "Caring for tropical cats" },
            new Post { Title = "Types of ornamental cats" }
        }
    });

执行两个查询时,可以观察到此问题:

var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();

在上述设置中,第一个查询返回所有 6 个Post,但第二个查询仅返回 3 个。 发生这种不匹配的原因是 Include 第二个查询中的方法加载相关 Blog 实体。 由于需要在两者之间BlogPost进行导航,因此 EF Core 在构造查询时使用INNER JOIN

SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
    SELECT [b].[BlogId], [b].[Name], [b].[Url]
    FROM [Blogs] AS [b]
    WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]

使用INNER JOIN过滤器可以排除所有其相关Post已被全局查询过滤器删除的Blog

可以通过使用可选导航而不是必需导航来解决这个问题。 这样,第一个查询将保持不变,但第二个查询现在将生成 LEFT JOIN 并返回 6 个结果。

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

替代方法是在两者 BlogPost 实体上指定一致的筛选器。 这样,匹配的筛选器会应用于 BlogPostPost会导致状态意外的那些项目被移除后,两个查询都返回 3 个结果。

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));

禁用筛选器

可以使用 IgnoreQueryFilters 运算符为单个 LINQ 查询禁用筛选器。

blogs = await db.Blogs
    .Include(b => b.Posts)
    .IgnoreQueryFilters()
    .ToListAsync();

局限性

全局查询筛选器具有以下限制:

  • 只能为继承层次结构的根实体类型定义筛选器。