全局查询筛选器Global Query Filters

备注

EF Core 2.0 中已引入此功能。This feature was introduced in EF Core 2.0.

全局查询筛选器是应用于元数据模型(通常为 OnModelCreating)中的实体类型的 LINQ 查询谓词。Global query filters are LINQ query predicates applied to Entity Types in the metadata model (usually in OnModelCreating). 查询谓词即通常传递给 LINQ Where 查询运算符的布尔表达式。A query predicate is a boolean expression typically passed to the LINQ Where query operator. EF Core 会自动将此类筛选器应用于涉及这些实体类型的任何 LINQ 查询。EF Core applies such filters automatically to any LINQ queries involving those Entity Types. EF Core 还将其应用于使用 Include 或导航属性进行间接引用的实体类型。EF Core also applies them to Entity Types, referenced indirectly through use of Include or navigation property. 此功能的一些常见应用如下:Some common applications of this feature are:

  • 软删除 - 实体类型定义“IsDeleted”属性。Soft delete - An Entity Type defines an IsDeleted property.
  • 多租户 - 实体类型定义“TenantId”属性。Multi-tenancy - An Entity Type defines a TenantId property.

示例Example

下面的示例演示了如何使用全局查询筛选器在简单的博客模型中实现多租户和软删除查询行为。The following example shows how to use Global Query Filters to implement multi-tenancy and soft-delete query behaviors in a simple blogging model.

提示

可在 GitHub 上查看多租户示例使用导航的示例You can view a multi-tenancy sample and samples using navigations on GitHub.

首先,定义实体:First, define the entities:

public class Blog
{
    private string _tenantId;

    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; }
}

请注意 Blog 实体上的 tenantId 字段的声明。Note the declaration of a tenantId field on the Blog entity. 此字段用于将每个 Blog 实例与特定租户相关联。This field will be used to associate each Blog instance with a specific tenant. 同时在 Post 实体类型上定义了 IsDeleted 属性。Also defined is an IsDeleted property on the Post entity type. 此属性用于跟踪某个 Post 实例是否已“软删除”。This property is used to keep track of whether a Post instance has been "soft-deleted". 也就是说,实例只是被标记为已删除,而非真正删除了基础数据。That is, the instance is marked as deleted without physically removing the underlying data.

接下来,使用 HasQueryFilter API 在 OnModelCreating 中配置查询筛选器。Next, configure the query filters in OnModelCreating using the HasQueryFilter API.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property<string>("_tenantId").HasColumnName("TenantId");

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

传递给 HasQueryFilter 调用的谓词表达式将立即自动应用于这些类型的任何 LINQ 查询。The predicate expressions passed to the HasQueryFilter calls will now automatically be applied to any LINQ queries for those types.

提示

请注意 DbContext 实例级别字段的使用:_tenantId 用于设置当前租户。Note the use of a DbContext instance level field: _tenantId used to set the current tenant. 模型级筛选器将使用正确上下文实例(即执行查询的实例)中的值。Model-level filters will use the value from the correct context instance (that is, the instance that is executing the query).

备注

目前不能在同一个实体中定义多个查询筛选器,只会应用最后一个筛选器。It is currently not possible to define multiple query filters on the same entity - only the last one will be applied. 但是,可以使用逻辑 AND 运算符(C# 中为 &&)定义含有多种条件的单个筛选器。However, you can define a single filter with multiple conditions using the logical AND operator (&& in C#).

使用导航Use of navigations

还可以在定义全局查询筛选器时使用导航。You can also use navigations in defining global query filters. 在查询筛选器中使用导航将导致以递归方式应用查询筛选器这一结果。Using navigations in query filter will cause query filters to be applied recursively. 当 EF Core 展开查询筛选器中使用的导航时,它还会应用在引用的实体上定义的查询筛选器。When EF Core expands navigations used in query filters, it will also apply query filters defined on referenced entities.

备注

目前,EF Core 不会检测全局查询筛选器定义中的循环,因此需在定义它们时小心谨慎。Currently EF Core does not detect cycles in global query filter definitions, so you should be careful when defining them. 如果指定错误,这些循环可能在查询转换期间导致无限循环。If specified incorrectly, cycles could lead to infinite loops during query translation.

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

注意

如果使用必需的导航访问定义了全局查询筛选器的实体,则可能导致意外结果。Using required navigation to access entity which has global query filter defined may lead to unexpected results.

必需的导航要求始终存在相关实体。Required navigation expects the related entity to always be present. 如果查询筛选器筛选出了必需的相关实体,则父实体不会出现在结果中。If necessary related entity is filtered out by the query filter, the parent entity wouldn't be in result either. 因此,结果中的元素数量可能会低于预期。So you may get fewer elements than expected in result.

为了说明此问题,我们可使用上面指定的 BlogPost 实体,同时使用下面的 OnModelCreating 方法:To illustrate the problem, we can use the Blog and Post entities specified above and the following OnModelCreating method:

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

可通过以下数据对模型进行种子设定:The model can be seeded with the following data:

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" }
        }
    });

执行两个查询时,可能会观察到此问题:The problem can be observed when executing two queries:

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

在上述设置中,第一个查询返回全部 6 个 Post,而第二个查询仅返回 3 个。With above setup, the first query returns all 6 Posts, however the second query only returns 3. 发生这种不匹配情况的原因是第二个查询中的 Include 方法会加载相关的 Blog 实体。This mismatch happens because Include method in the second query loads the related Blog entities. 由于需要在 BlogPost 之间导航,因此在构造查询时,EF Core 使用了 INNER JOINSince the navigation between Blog and Post is required, EF Core uses INNER JOIN when constructing the query:

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

使用 INNER JOIN 会筛选出其相关 Blog 已被全局查询筛选器删除的所有 PostUse of the INNER JOIN filters out all Posts whose related Blogs have been removed by a global query filter.

可使用可选导航来解决此问题,而不使用必需导航。It can be addressed by using optional navigation instead of required. 这样一来,第一个查询与之前相同,但第二个查询现将生成 LEFT JOIN 并返回 6 个结果。This way the first query stays the same as before, however the second query will now generate LEFT JOIN and return 6 results.

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

替代方法是在 BlogPost 实体上指定一致的筛选器。Alternative approach is to specify consistent filters on both Blog and Post entities. 这样,匹配的筛选器就会同时应用于 BlogPostThis way matching filters are applied to both Blog and Post. 可能导致出现意外状态的 Post 已被删除,且两个查询都返回 3 个结果。Posts that could end up in unexpected state are removed and both queries return 3 results.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    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"));
}

禁用筛选器Disabling Filters

可使用 IgnoreQueryFilters() 运算符对各个 LINQ 查询禁用筛选器。Filters may be disabled for individual LINQ queries by using the IgnoreQueryFilters() operator.

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

限制Limitations

全局查询筛选器具有以下限制:Global query filters have the following limitations:

  • 仅可为继承层次结构的根实体类型定义筛选器。Filters can only be defined for the root Entity Type of an inheritance hierarchy.