Skip to content

全局查询过滤器 Global Query Filters ​

目录 ​


什么是全局查询过滤器 ​

概念理解 ​

全局查询过滤器(Global Query Filters) 是 EF Core 2.0+ 引入的功能,允许在 DbContext 级别为实体类型定义自动应用的 LINQ 查询条件,所有查询都会自动附加此过滤条件。

csharp
// 典型应用: 软删除
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }  // 软删除标记
}

// 配置全局过滤器
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);  // ← 自动过滤已删除数据
}

// 效果: 所有查询自动应用过滤
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE IsDeleted = 0
// ↑ 无需手动写 Where(!p.IsDeleted)

// 即使这样写也会自动追加条件
var expensiveProducts = await context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();
// SQL: SELECT * FROM Products WHERE Price > 100 AND IsDeleted = 0
//      ↑ 自动添加 IsDeleted 条件!

工作原理 ​

mermaid
graph TB
    A[LINQ 查询] --> B[EF Core 处理]
    B --> C{是否有全局过滤器?}
    C -->|是| D[自动追加过滤条件]
    C -->|否| E[直接生成 SQL]
    D --> F[合并表达式树]
    F --> G[生成最终 SQL]
    G --> H[执行查询]
    E --> H

关键点:

  • ✅ 自动应用: 无需手动添加 Where 条件
  • ✅ 强制生效: 除非显式禁用,否则始终有效
  • ✅ 组合查询: 与其他 Where 条件自动合并(AND)
  • ✅ 继承支持: 基类的过滤器应用于所有派生类

为什么需要全局过滤器? ​

问题: 手动过滤容易遗漏 ​

csharp
// ❌ 没有全局过滤器: 需要每次都手动过滤
public class ProductService
{
    private readonly AppDbContext _context;
    
    public async Task<List<Product>> GetActiveProductsAsync()
    {
        return await _context.Products
            .Where(p => !p.IsDeleted)  // ← 必须记得加这个
            .ToListAsync();
    }
    
    public async Task<Product> GetProductByIdAsync(int id)
    {
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id && !p.IsDeleted);  // ← 也要记得加
    }
    
    public async Task<List<Product>> SearchProductsAsync(string keyword)
    {
        return await _context.Products
            .Where(p => p.Name.Contains(keyword))  // 💥 忘记过滤!
            .ToListAsync();
        // 返回了已删除的产品!
    }
}

// 问题:
// - 容易遗漏过滤条件
// - 代码重复
// - 维护困难
// - Bug 风险高

解决: 使用全局过滤器 ​

csharp
// ✅ 有全局过滤器: 自动过滤,不会遗漏
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);
}

public class ProductService
{
    private readonly AppDbContext _context;
    
    public async Task<List<Product>> GetProductsAsync()
    {
        return await _context.Products.ToListAsync();
        // ✅ 自动过滤 IsDeleted = true 的记录
    }
    
    public async Task<Product> GetProductByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
        // ✅ Find 也应用过滤器,找不到已删除的产品
    }
    
    public async Task<List<Product>> SearchProductsAsync(string keyword)
    {
        return await _context.Products
            .Where(p => p.Name.Contains(keyword))
            .ToListAsync();
        // ✅ 即使忘记写过滤,也会自动应用
    }
}

基本使用方法 ​

简单过滤器 ​

示例 1: 软删除 ​

csharp
public class BaseEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

public class Product : BaseEntity
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order : BaseEntity
{
    public DateTime OrderDate { get; set; }
    public decimal TotalAmount { get; set; }
}

// 为所有继承 BaseEntity 的实体配置过滤器
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 方式 1: 逐个配置
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);
    
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => !o.IsDeleted);
    
    // 方式 2: 批量配置(.NET 8+)
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
        {
            var parameter = Expression.Parameter(entityType.ClrType, "e");
            var property = Expression.Property(parameter, nameof(BaseEntity.IsDeleted));
            var notExpression = Expression.Not(property);
            var lambda = Expression.Lambda(notExpression, parameter);
            
            entityType.SetQueryFilter(lambda);
        }
    }
}

示例 2: 多租户隔离 ​

csharp
public interface IMultiTenant
{
    int TenantId { get; set; }
}

public class Product : IMultiTenant
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int TenantId { get; set; }  // 租户 ID
}

public class Order : IMultiTenant
{
    public int Id { get; set; }
    public int TenantId { get; set; }
}

// DbContext
public class AppDbContext : DbContext
{
    private readonly int _currentTenantId;
    
    public AppDbContext(DbContextOptions<AppDbContext> options, int currentTenantId) 
        : base(options)
    {
        _currentTenantId = currentTenantId;
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 为所有实现 IMultiTenant 的实体配置租户过滤
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(IMultiTenant).IsAssignableFrom(entityType.ClrType))
            {
                var parameter = Expression.Parameter(entityType.ClrType, "e");
                var property = Expression.Property(parameter, nameof(IMultiTenant.TenantId));
                var tenantValue = Expression.Constant(_currentTenantId);
                var equalExpression = Expression.Equal(property, tenantValue);
                var lambda = Expression.Lambda(equalExpression, parameter);
                
                entityType.SetQueryFilter(lambda);
            }
        }
    }
}

// 效果: 每个租户只能看到自己的数据
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE TenantId = @currentTenantId

示例 3: 状态过滤 ​

csharp
public enum RecordStatus
{
    Draft = 0,
    Published = 1,
    Archived = 2,
    Deleted = 3
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public RecordStatus Status { get; set; }
}

// 只显示已发布的文章
modelBuilder.Entity<Article>()
    .HasQueryFilter(a => a.Status == RecordStatus.Published);

// 用户查询时自动只返回已发布文章
var articles = await context.Articles.ToListAsync();
// SQL: SELECT * FROM Articles WHERE Status = 1

复杂过滤器 ​

组合条件 ​

csharp
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsPublished { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? PublishedAt { get; set; }
    public DateTime? ExpiresAt { get; set; }
}

// 多重条件过滤
modelBuilder.Entity<BlogPost>()
    .HasQueryFilter(post => 
        !post.IsDeleted &&                    // 未删除
        post.IsPublished &&                   // 已发布
        (!post.ExpiresAt.HasValue ||          // 未过期或未设置过期时间
         post.ExpiresAt.Value > DateTime.UtcNow));

// 查询自动应用所有条件
var posts = await context.BlogPosts.ToListAsync();
// SQL: WHERE IsDeleted = 0 AND IsPublished = 1 
//      AND (ExpiresAt IS NULL OR ExpiresAt > GETUTCDATE())

关联实体过滤 ​

csharp
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public ICollection<Product> Products { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
    public bool IsDeleted { get; set; }
}

// 产品: 过滤已删除
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);

// 分类: 过滤非活跃分类
modelBuilder.Entity<Category>()
    .HasQueryFilter(c => c.IsActive);

// Include 也会应用过滤器
var categories = await context.Categories
    .Include(c => c.Products)
    .ToListAsync();
// 分类: WHERE IsActive = 1
// 产品: WHERE IsDeleted = 0

高级应用场景 ​

场景 1: 临时禁用过滤器 ​

csharp
// 有时需要查询所有数据(包括已删除)
// 方法 1: IgnoreQueryFilters
var allProducts = await context.Products
    .IgnoreQueryFilters()  // ← 禁用所有全局过滤器
    .ToListAsync();
// SQL: SELECT * FROM Products (无 WHERE 条件)

// 方法 2: 包含已删除产品的特殊查询
var deletedProducts = await context.Products
    .IgnoreQueryFilters()
    .Where(p => p.IsDeleted)
    .ToListAsync();

// ⚠️ 注意: IgnoreQueryFilters 会禁用该查询的所有过滤器

场景 2: 动态过滤器 ​

csharp
public class AppDbContext : DbContext
{
    private readonly IUserContext _userContext;
    
    public AppDbContext(DbContextOptions<AppDbContext> options, 
                       IUserContext userContext) 
        : base(options)
    {
        _userContext = userContext;
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 根据当前用户角色动态过滤
        modelBuilder.Entity<Document>()
            .HasQueryFilter(doc => 
                doc.IsPublic ||                                    // 公开文档
                doc.OwnerId == _userContext.UserId ||              // 自己创建的
                (_userContext.IsAdmin && !doc.IsDeleted));         // 管理员(排除已删除)
    }
}

// 不同用户看到不同的数据
// 普通用户: 公开文档 + 自己的文档
// 管理员: 所有未删除文档

场景 3: 审计日志过滤 ​

csharp
public class AuditLog
{
    public int Id { get; set; }
    public string Action { get; set; }
    public DateTime Timestamp { get; set; }
    public int RetentionDays { get; set; }  // 保留天数
}

// 自动过滤已过期的审计日志
modelBuilder.Entity<AuditLog>()
    .HasQueryFilter(log => 
        log.RetentionDays == 0 ||  // 永久保留
        log.Timestamp.AddDays(log.RetentionDays) > DateTime.UtcNow);

// 查询只返回有效期内的日志
var logs = await context.AuditLogs.ToListAsync();

场景 4: 版本控制 ​

csharp
public class Document
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int Version { get; set; }
    public bool IsCurrentVersion { get; set; }
}

// 默认只显示当前版本
modelBuilder.Entity<Document>()
    .HasQueryFilter(d => d.IsCurrentVersion);

// 查询自动返回最新版本
var docs = await context.Documents.ToListAsync();

// 查看历史版本需要禁用过滤器
var history = await context.Documents
    .IgnoreQueryFilters()
    .Where(d => d.Id == docId)
    .OrderByDescending(d => d.Version)
    .ToListAsync();

场景 5: 多语言过滤 ​

csharp
public class LocalizedContent
{
    public int Id { get; set; }
    public string EntityId { get; set; }
    public string Language { get; set; }
    public string Content { get; set; }
}

public class AppDbContext : DbContext
{
    private readonly string _currentLanguage;
    
    public AppDbContext(DbContextOptions<AppDbContext> options, 
                       string currentLanguage) 
        : base(options)
    {
        _currentLanguage = currentLanguage;
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<LocalizedContent>()
            .HasQueryFilter(lc => lc.Language == _currentLanguage);
    }
}

// 根据用户语言偏好自动过滤
var content = await context.LocalizedContents.ToListAsync();
// 中文用户: WHERE Language = 'zh-CN'
// 英文用户: WHERE Language = 'en-US'

性能优化与注意事项 ​

性能影响 ​

正面影响 ​

✅ 减少数据传输:

csharp
// 没有过滤器: 传输 1000 条,客户端过滤 900 条
var products = await context.Products.ToListAsync();  // 1000 条
var active = products.Where(p => !p.IsDeleted).ToList();  // 100 条

// 有过滤器: 只传输 100 条
var products = await context.Products.ToListAsync();  // 100 条(自动过滤)
// 节省 90% 网络带宽!

✅ 索引优化:

csharp
// 为过滤器字段创建索引
modelBuilder.Entity<Product>()
    .HasIndex(p => p.IsDeleted);

// 过滤器查询可以利用索引,速度极快

潜在问题 ​

⚠️ 意外行为:

csharp
// 陷阱: Find 方法也应用过滤器
var product = await context.Products.FindAsync(1);
// 如果 ID=1 的产品已删除,返回 null(可能不符合预期)

// 解决: 明确使用 IgnoreQueryFilters
var product = await context.Products
    .IgnoreQueryFilters()
    .FirstOrDefaultAsync(p => p.Id == 1);

⚠️ 聚合查询:

csharp
// 统计总数时也要注意过滤器
var totalCount = await context.Products.CountAsync();
// 只统计未删除的产品

// 如果需要总数(包括已删除)
var totalWithDeleted = await context.Products
    .IgnoreQueryFilters()
    .CountAsync();

监控过滤器应用 ​

csharp
// 启用日志查看过滤器应用情况
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information);
});

// 输出:
// info: Applying query filter 'IsDeleted' to entity 'Product'
// info: Generated SQL: SELECT ... WHERE IsDeleted = 0

.NET 8/9/10 新特性 ​

.NET 8: 改进的过滤器诊断 ​

csharp
// .NET 8 增强了过滤器相关的诊断信息
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .EnableDetailedErrors()
           .LogTo(Console.WriteLine, LogLevel.Debug);
});

// 当检测到可能的过滤器问题时输出警告:
// warn: Query filter on 'Product' may cause performance issues. 
//       Consider adding an index on 'IsDeleted' column.

.NET 9: 动态过滤器增强 ​

csharp
// .NET 9 支持更灵活的动态过滤器
modelBuilder.Entity<Product>()
    .HasQueryFilter((product, ctx) => 
    {
        var userContext = ctx.GetService<IUserContext>();
        return !product.IsDeleted || userContext.IsAdmin;
    });

// 可以直接访问服务容器,无需闭包捕获

.NET 10: 智能过滤器优化(路线图) ​

预计特性:

  • 基于 AI 的过滤器性能分析
  • 自动建议索引优化
  • 运行时动态调整过滤器
  • 过滤器依赖关系可视化

最佳实践与陷阱 ​

最佳实践 ​

1. 为过滤器字段创建索引 ​

csharp
// ✅ 推荐: 索引过滤器字段
modelBuilder.Entity<Product>()
    .HasIndex(p => p.IsDeleted);

modelBuilder.Entity<Order>()
    .HasIndex(o => o.TenantId);

// 提升过滤器查询性能 10-100x

2. 保持一致性 ​

csharp
// ✅ 推荐: 所有相关实体都应用一致的过滤器
modelBuilder.Entity<Category>()
    .HasQueryFilter(c => !c.IsDeleted);

modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);

// 避免: 分类过滤了但产品没过滤

3. 文档化过滤器 ​

csharp
/// <summary>
/// 产品 DbSet
/// 注意: 应用了全局过滤器,自动排除 IsDeleted = true 的记录
/// 如需查询所有记录(包括已删除),使用 IgnoreQueryFilters()
/// </summary>
public DbSet<Product> Products { get; set; }

4. 测试过滤器行为 ​

csharp
[Fact]
public async Task GlobalFilter_ShouldExcludeDeletedProducts()
{
    // Arrange
    await SeedTestDataAsync();
    
    // Act
    var products = await _context.Products.ToListAsync();
    
    // Assert
    Assert.DoesNotContain(products, p => p.IsDeleted);
}

[Fact]
public async Task IgnoreQueryFilters_ShouldIncludeDeletedProducts()
{
    // Arrange
    await SeedTestDataAsync();
    
    // Act
    var allProducts = await _context.Products
        .IgnoreQueryFilters()
        .ToListAsync();
    
    // Assert
    Assert.Contains(allProducts, p => p.IsDeleted);
}

常见陷阱 ​

陷阱 1: 忘记过滤器存在 ​

csharp
// ❌ 错误: 期望返回所有记录但实际被过滤
var count = await context.Products.CountAsync();
Console.WriteLine($"总产品数: {count}");  // 实际是"未删除产品数"

// ✅ 正确: 明确意图
var activeCount = await context.Products.CountAsync();
var totalCount = await context.Products
    .IgnoreQueryFilters()
    .CountAsync();

陷阱 2: 过滤器冲突 ​

csharp
// ❌ 错误: 多个过滤器可能导致意外结果
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);

modelBuilder.Entity<Product>()
    .HasQueryFilter(p => p.Category.IsActive);  // 💥 覆盖前一个过滤器!

// ✅ 正确: 合并为一个过滤器
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted && p.Category.IsActive);

陷阱 3: 导航属性过滤不一致 ​

csharp
// ❌ 错误: 主实体有过滤器,导航属性没有
modelBuilder.Entity<Category>()
    .HasQueryFilter(c => !c.IsDeleted);

// Product 没有过滤器
var categories = await context.Categories
    .Include(c => c.Products)  // 💥 包含已删除的产品!
    .ToListAsync();

// ✅ 正确: 都为导航属性配置过滤器
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);

陷阱 4: 性能问题 ​

csharp
// ⚠️ 注意: 复杂的过滤器可能影响性能
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => 
        !p.IsDeleted && 
        p.Tags.Any(t => t.IsActive) &&  // 💥 子查询,性能差
        p.Category.IsActive);

// ✅ 优化: 简化过滤器,在查询时添加额外条件
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);  // 简单高效

// 需要额外条件时在查询中添加
var products = await context.Products
    .Where(p => p.Category.IsActive)
    .ToListAsync();

总结 ​

核心要点 ​

  1. 自动应用: 全局过滤器自动附加到所有查询
  2. 强制生效: 除非显式禁用,否则始终有效
  3. 典型场景: 软删除、多租户、状态过滤
  4. 性能优化: 为过滤器字段创建索引
  5. 谨慎使用: 避免过度复杂的过滤逻辑

应用场景对比 ​

场景过滤器复杂度性能影响推荐程度
软删除简单(布尔值)低⭐⭐⭐⭐⭐
多租户简单(整数)低⭐⭐⭐⭐⭐
状态过滤中等(枚举)中⭐⭐⭐⭐
权限过滤复杂(服务依赖)中高⭐⭐⭐
多语言简单(字符串)低⭐⭐⭐⭐

决策流程 ​

是否需要自动过滤?
├─ 是 → 使用 HasQueryFilter
│   ├─ 简单条件 → 直接配置
│   └─ 复杂条件 → 考虑是否在查询时处理
└─ 否 → 手动添加 Where 条件

是否需要偶尔查看所有数据?
├─ 是 → 使用 IgnoreQueryFilters()
└─ 否 → 保持过滤器始终生效

代码模板 ​

csharp
// 标准模板: 软删除
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 1. 配置过滤器
    modelBuilder.Entity<TEntity>()
        .HasQueryFilter(e => !e.IsDeleted);
    
    // 2. 创建索引
    modelBuilder.Entity<TEntity>()
        .HasIndex(e => e.IsDeleted);
}

// 查询时
var entities = await context.Entities.ToListAsync();  // 自动过滤

// 需要查看所有数据
var allEntities = await context.Entities
    .IgnoreQueryFilters()
    .ToListAsync();

下一步 ​

基于 MIT 许可发布