全局查询筛选器Global Query Filters

备注

EF Core 2.0 中已引入此功能。

全局查询筛选器是应用于元数据模型(通常为 OnModelCreating)中的实体类型的 LINQ 查询谓词(通常传递给 LINQ Where 查询运算符的布尔表达式)。 此类筛选器自动应用于涉及这些实体类型(包括通过使用 Include 或直接导航属性引用等方式间接引用的实体类型)的所有 LINQ 查询。 此功能的一些常见应用如下:

  • 软删除 - 实体类型定义“IsDeleted”属性。
  • 多租户 - 实体类型定义“TenantId”属性。

示例Example

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

提示

可在 GitHub 上查看多租户示例使用导航的示例

首先,定义实体:

  1. public class Blog
  2. {
  3. private string _tenantId;
  4. public int BlogId { get; set; }
  5. public string Name { get; set; }
  6. public string Url { get; set; }
  7. public List<Post> Posts { get; set; }
  8. }
  9. public class Post
  10. {
  11. public int PostId { get; set; }
  12. public string Title { get; set; }
  13. public string Content { get; set; }
  14. public bool IsDeleted { get; set; }
  15. public Blog Blog { get; set; }
  16. }

请注意 Blog 实体上的 tenantId 字段的声明。 这会用于将每个 Blog 实例与特定租户相关联。 同时在 Post 实体类型上定义了 IsDeleted 属性。 这会用于跟踪一个 Post 实例是否已“软删除”。 也就是说,实例只是被标记为已删除,而非真正删除了基础数据。

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

  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3. modelBuilder.Entity<Blog>().Property<string>("_tenantId").HasColumnName("TenantId");
  4. // Configure entity filters
  5. modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
  6. modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
  7. }

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

提示

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

备注

目前不能在同一个实体中定义多个查询筛选器,只会应用最后一个筛选器。 但是,可以使用逻辑 AND 运算符(C# 中为 &&)定义含有多种条件的单个筛选器。

使用导航Use of navigations

可在定义全局查询筛选器时使用导航。 以递归的方式应用它们 - 转换查询筛选器中使用的导航时,还将应用在引用的实体上定义的查询筛选器,这可能会添加更多导航。

备注

目前,EF Core 不会检测全局查询筛选器定义中的循环,因此需在定义它们时小心谨慎。 如果指定错误,这可能在查询转换期间导致无限循环。

使用必需的导航访问具有查询筛选器的实体Accessing entity with query filter using reqiured navigation

注意

如果使用必需的导航访问定义了全局查询筛选器的实体,则可能导致意外结果。

必需的导航要求始终存在相关实体。 如果查询筛选器筛选出了必需的相关实体,则父实体可能导致出现意外状态。 这可能导致返回的元素数量比预期数量少。

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

  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3. modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
  4. modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
  5. }

可通过以下数据对模型进行种子设定:

  1. db.Blogs.Add(
  2. new Blog
  3. {
  4. Url = "http://sample.com/blogs/fish",
  5. Posts = new List<Post>
  6. {
  7. new Post { Title = "Fish care 101" },
  8. new Post { Title = "Caring for tropical fish" },
  9. new Post { Title = "Types of ornamental fish" }
  10. }
  11. });
  12. db.Blogs.Add(
  13. new Blog
  14. {
  15. Url = "http://sample.com/blogs/cats",
  16. Posts = new List<Post>
  17. {
  18. new Post { Title = "Cat care 101" },
  19. new Post { Title = "Caring for tropical cats" },
  20. new Post { Title = "Types of ornamental cats" }
  21. }
  22. });

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

  1. var allPosts = db.Posts.ToList();
  2. var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();

在此设置中,第一个查询返回全部 6 个 Post,而第二个查询仅返回 3 个。 发生这种情况的原因是第二个查询中的 Include 方法会加载相关的 Blog 实体。 由于需要在 BlogPost 之间导航,因此在构造查询时,EF Core 使用了 INNER JOIN

  1. SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
  2. FROM [Post] AS [p]
  3. INNER JOIN (
  4. SELECT [b].[BlogId], [b].[Name], [b].[Url]
  5. FROM [Blogs] AS [b]
  6. WHERE CHARINDEX(N'fish', [b].[Url]) > 0
  7. ) AS [t] ON [p].[BlogId] = [t].[BlogId]

使用 INNER JOIN 会筛选出其相关 Blog 已被全局查询筛选器删除的所有 Post

可使用可选导航来解决此问题,而不使用必需导航。 这样一来,第一个查询与之前相同,但第二个查询现将生成 LEFT JOIN 并返回 6 个结果。

  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3. modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
  4. modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
  5. }

替代方法是在 BlogPost 实体上指定一致的筛选器。 这样,匹配的筛选器就会同时应用于 BlogPost。 可能导致出现意外状态的 Post 已被删除,且两个查询都返回 3 个结果。

  1. protected override void OnModelCreating(ModelBuilder modelBuilder)
  2. {
  3. modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
  4. modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
  5. modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
  6. }

禁用筛选器Disabling Filters

可使用 IgnoreQueryFilters() 运算符对各个 LINQ 查询禁用筛选器。

  1. blogs = db.Blogs
  2. .Include(b => b.Posts)
  3. .IgnoreQueryFilters()
  4. .ToList();

限制Limitations

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

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