Skip to content

全局查询过滤器 ​

概述 ​

全局查询过滤器(Global Query Filters)是在 OnModelCreating 中定义的 LINQ 查询谓词,自动应用于指定实体类型的所有查询。这是实现软删除、多租户隔离等功能的核心机制。

核心优势 ​

特性说明
自动应用无需手动添加 WHERE 条件
集中管理在一处定义,全局生效
可覆盖可使用 IgnoreQueryFilters() 临时禁用
组合性多个过滤器自动组合(AND)
性能优化在数据库层面过滤,非客户端

典型应用场景 ​

  • ✅ 软删除: 自动排除已删除的记录
  • ✅ 多租户: 自动隔离租户数据
  • ✅ 状态过滤: 仅显示激活/有效的记录
  • ✅ 权限控制: 根据用户角色过滤数据
  • ✅ 时间范围: 仅查询有效期内的数据

基础用法 ​

软删除实现 ​

csharp
// BaseEntity.cs - 基类
public abstract class BaseEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

// Product.cs
public class Product : BaseEntity
{
    public string Name { get; set; } = null!;
    public decimal Price { get; set; }
}

// AppDbContext.cs
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // ✅ 全局过滤器: 自动排除已删除的记录
        modelBuilder.Entity<Product>()
            .HasQueryFilter(p => !p.IsDeleted);
        
        // 对其他实体也应用
        modelBuilder.Entity<Category>()
            .HasQueryFilter(c => !c.IsDeleted);
    }
}

// 使用
var products = await context.Products.ToListAsync();
// 生成的 SQL 自动包含: WHERE [IsDeleted] = 0

// ❌ 无法查询到已删除的记录
var deletedProduct = await context.Products
    .FirstOrDefaultAsync(p => p.Id == deletedId);  // 返回 null

临时禁用过滤器 ​

csharp
// 需要查询已删除的记录时
var allProducts = await context.Products
    .IgnoreQueryFilters()  // ← 禁用全局过滤器
    .ToListAsync();

// 包括已删除的
var deletedProducts = await context.Products
    .IgnoreQueryFilters()
    .Where(p => p.IsDeleted)
    .ToListAsync();

多租户隔离 ​

每租户数据隔离 ​

csharp
// TenantEntity.cs
public abstract class TenantEntity
{
    public int Id { get; set; }
    public string TenantId { get; set; } = null!;
}

// Order.cs
public class Order : TenantEntity
{
    public DateTime OrderDate { get; set; }
    public decimal TotalAmount { get; set; }
}

// AppDbContext.cs
public class AppDbContext : DbContext
{
    private readonly string _currentTenantId;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        IHttpContextAccessor httpContextAccessor)
        : base(options)
    {
        // 从 HTTP 上下文获取当前租户 ID
        _currentTenantId = httpContextAccessor.HttpContext?
            .User.FindFirst("tenant_id")?.Value 
            ?? throw new InvalidOperationException("Tenant ID not found");
    }

    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // ✅ 全局过滤器: 自动隔离租户数据
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => o.TenantId == _currentTenantId);
    }
}

// 使用
var orders = await context.Orders.ToListAsync();
// 生成的 SQL 自动包含: WHERE [TenantId] = 'tenant-123'
// 用户只能看到自己租户的数据!

动态租户过滤器 ​

csharp
// ITenantProvider.cs
public interface ITenantProvider
{
    string GetCurrentTenantId();
}

// TenantProvider.cs
public class TenantProvider : ITenantProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetCurrentTenantId()
    {
        return _httpContextAccessor.HttpContext?
            .User.FindFirst("tenant_id")?.Value 
            ?? "default";
    }
}

// AppDbContext.cs
public class AppDbContext : DbContext
{
    private readonly ITenantProvider _tenantProvider;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantProvider = tenantProvider;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 使用提供者动态获取租户 ID
        var tenantId = _tenantProvider.GetCurrentTenantId();
        
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => o.TenantId == tenantId);
    }
}

组合过滤器 ​

软删除 + 多租户 ​

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => !o.IsDeleted && o.TenantId == _currentTenantId);
    
    // EF Core 自动组合为:
    // WHERE [IsDeleted] = 0 AND [TenantId] = @__currentTenantId_0
}

多个独立过滤器 ​

csharp
// EF Core 支持为同一实体定义多个过滤器
modelBuilder.Entity<Product>(entity =>
{
    // 过滤器 1: 软删除
    entity.HasQueryFilter(p => !p.IsDeleted);
    
    // 过滤器 2: 仅激活的产品
    entity.HasQueryFilter(p => p.IsActive);
    
    // 过滤器 3: 多租户
    entity.HasQueryFilter(p => p.TenantId == _currentTenantId);
});

// 最终生成的 SQL:
// WHERE [IsDeleted] = 0 AND [IsActive] = 1 AND [TenantId] = @__tenantId_0

高级场景 ​

基于角色的访问控制 ​

csharp
// UserRole.cs
public enum UserRole
{
    Admin,
    Manager,
    User
}

// Document.cs
public class Document
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string OwnerId { get; set; }
    public UserRole MinRequiredRole { get; set; }
    public bool IsPublic { get; set; }
}

// AppDbContext.cs
public class AppDbContext : DbContext
{
    private readonly string _currentUserId;
    private readonly UserRole _currentUserRole;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ICurrentUserProvider userProvider)
        : base(options)
    {
        _currentUserId = userProvider.CurrentUserId;
        _currentUserRole = userProvider.CurrentRole;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Document>(entity =>
        {
            // 管理员: 查看所有文档
            if (_currentUserRole == UserRole.Admin)
            {
                // 无过滤器
            }
            // 经理: 查看本部门 + 公共文档
            else if (_currentUserRole == UserRole.Manager)
            {
                entity.HasQueryFilter(d => 
                    d.OwnerId == _currentUserId || 
                    d.IsPublic);
            }
            // 普通用户: 仅查看自己的 + 公共文档
            else
            {
                entity.HasQueryFilter(d => 
                    d.OwnerId == _currentUserId || 
                    (d.IsPublic && d.MinRequiredRole <= _currentUserRole));
            }
        });
    }
}

时间范围过滤 ​

csharp
// Promotion.cs
public class Promotion
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

// AppDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var now = DateTime.UtcNow;
    
    modelBuilder.Entity<Promotion>()
        .HasQueryFilter(p => p.StartDate <= now && p.EndDate >= now);
}

// 使用
var activePromotions = await context.Promotions.ToListAsync();
// 仅返回当前有效的促销活动

// 查看所有(包括过期和未开始)
var allPromotions = await context.Promotions
    .IgnoreQueryFilters()
    .ToListAsync();

软删除审计 ​

csharp
// SoftDeleteEntity.cs
public abstract class SoftDeleteEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }
}

// AppDbContext.cs
public class AppDbContext : DbContext
{
    private readonly ICurrentUserProvider _userProvider;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ICurrentUserProvider userProvider)
        : base(options)
    {
        _userProvider = userProvider;
    }

    public override Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        // 拦截删除操作,转为软删除
        foreach (var entry in ChangeTracker.Entries<SoftDeleteEntity>())
        {
            if (entry.State == EntityState.Deleted)
            {
                entry.State = EntityState.Modified;
                entry.Entity.IsDeleted = true;
                entry.Entity.DeletedAt = DateTime.UtcNow;
                entry.Entity.DeletedBy = _userProvider.CurrentUserId;
            }
        }

        return base.SaveChangesAsync(ct);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 全局过滤器
        modelBuilder.Entity<Product>()
            .HasQueryFilter(p => !p.IsDeleted);
        
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => !o.IsDeleted);
    }
}

// 使用
var product = await context.Products.FindAsync(1);
context.Products.Remove(product);  // 实际是软删除
await context.SaveChangesAsync();

// 数据库中: IsDeleted = 1, DeletedAt = UTC时间, DeletedBy = 用户ID

继承与过滤器 ​

基类过滤器自动继承 ​

csharp
// BaseEntity.cs
public abstract class BaseEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
}

// Product.cs
public class Product : BaseEntity
{
    public string Name { get; set; }
}

// Category.cs
public class Category : BaseEntity
{
    public string Name { get; set; }
}

// AppDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 为基类配置过滤器
    modelBuilder.Entity<BaseEntity>()
        .HasQueryFilter(e => !e.IsDeleted);
    
    // ✅ Product 和 Category 自动继承该过滤器
}

// 使用
var products = await context.Products.ToListAsync();
// 自动包含: WHERE [IsDeleted] = 0

var categories = await context.Categories.ToListAsync();
// 自动包含: WHERE [IsDeleted] = 0

子类自定义过滤器 ​

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 基类过滤器
    modelBuilder.Entity<BaseEntity>()
        .HasQueryFilter(e => !e.IsDeleted);
    
    // 子类额外过滤器
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => p.IsActive);  // 叠加到基类过滤器
    
    // 最终 Product 的过滤器:
    // !IsDeleted AND IsActive
}

性能优化 ​

索引优化 ​

csharp
// 确保过滤器使用的列有索引
modelBuilder.Entity<Product>(entity =>
{
    // 全局过滤器使用 IsDeleted
    entity.HasQueryFilter(p => !p.IsDeleted);
    
    // ✅ 添加索引
    entity.HasIndex(p => p.IsDeleted);
    
    // 多租户场景
    entity.HasQueryFilter(p => p.TenantId == _tenantId);
    entity.HasIndex(p => p.TenantId);
    
    // 组合索引(多租户 + 软删除)
    entity.HasIndex(p => new { p.TenantId, p.IsDeleted });
});

避免复杂表达式 ​

csharp
// ❌ 错误: 复杂表达式影响性能
entity.HasQueryFilter(p => 
    !p.IsDeleted && 
    p.Categories.Any(c => c.IsActive) &&  // ⚠️ 子查询
    p.Prices.OrderByDescending(pr => pr.Date).First().Amount > 100);  // ⚠️ 复杂计算

// ✅ 正确: 简化过滤器
entity.HasQueryFilter(p => !p.IsDeleted && p.IsActive);

// 复杂逻辑在查询时显式处理
var products = await context.Products
    .Where(p => p.Prices.OrderByDescending(pr => pr.Date)
                        .First().Amount > 100)
    .ToListAsync();

常见问题与解决方案 ​

问题 1: 过滤器无法禁用 ​

csharp
// ❌ 错误: IgnoreQueryFilters 位置错误
var products = await context.Products
    .Where(p => p.Price > 100)
    .IgnoreQueryFilters()  // ⚠️ 必须在 Where 之前
    .ToListAsync();

// ✅ 正确
var products = await context.Products
    .IgnoreQueryFilters()  // ← 在最前面
    .Where(p => p.Price > 100)
    .ToListAsync();

问题 2: 导航属性不应用过滤器 ​

csharp
// ❌ 错误: Include 的导航属性不应用过滤器
var order = await context.Orders
    .Include(o => o.Items)  // ⚠️ OrderItems 的过滤器不生效
    .FirstOrDefaultAsync(o => o.Id == orderId);

// ✅ 修复: 显式过滤
var order = await context.Orders
    .Include(o => o.Items.Where(i => !i.IsDeleted))  // 显式过滤
    .FirstOrDefaultAsync(o => o.Id == orderId);

// 或分别加载
var order = await context.Orders.FirstAsync(o => o.Id == orderId);
await context.Entry(order)
    .Collection(o => o.Items)
    .Query()
    .Where(i => !i.IsDeleted)
    .LoadAsync();

问题 3: 动态值捕获问题 ​

csharp
// ❌ 错误: 过滤器中的值在启动时捕获
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var tenantId = GetTenantId();  // ⚠️ 仅在启动时调用一次
    
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => o.TenantId == tenantId);  // 固定值!
}

// ✅ 正确: 使用字段/属性
private string _currentTenantId = null!;

public void SetTenantId(string tenantId)
{
    _currentTenantId = tenantId;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => o.TenantId == _currentTenantId);  // 引用字段
}

最佳实践 ​

✅ 推荐做法 ​

1. 使用基类统一管理 ​

csharp
// ✅ 所有实体继承基类
public abstract class AuditableEntity
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public string TenantId { get; set; } = null!;
    public DateTime CreatedAt { get; set; }
}

// 统一配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<AuditableEntity>()
        .HasQueryFilter(e => !e.IsDeleted && e.TenantId == _tenantId);
}

2. 提供便捷的禁用方法 ​

csharp
// ExtensionMethods.cs
public static class DbContextExtensions
{
    public static IQueryable<T> WithDeleted<T>(this DbSet<T> dbSet) 
        where T : BaseEntity
    {
        return dbSet.IgnoreQueryFilters();
    }
    
    public static IQueryable<T> OnlyDeleted<T>(this DbSet<T> dbSet) 
        where T : BaseEntity
    {
        return dbSet.IgnoreQueryFilters()
                    .Where(e => e.IsDeleted);
    }
}

// 使用
var deletedProducts = context.Products.OnlyDeleted().ToList();

3. 记录过滤器变更 ​

csharp
// 审计日志
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
    foreach (var entry in ChangeTracker.Entries())
    {
        if (entry.Entity is IEntityWithFilters && 
            entry.State == EntityState.Modified)
        {
            _logger.LogInformation(
                "Entity {Type} modified. Filters may affect visibility.",
                entry.Entity.GetType().Name);
        }
    }
    
    return base.SaveChangesAsync(ct);
}

❌ 避免的错误 ​

1. 不要在过滤器中执行副作用 ​

csharp
// ❌ 错误
entity.HasQueryFilter(p => 
{
    LogAccess(p.Id);  // ⚠️ 副作用!
    return !p.IsDeleted;
});

// ✅ 正确: 过滤器应该是纯函数
entity.HasQueryFilter(p => !p.IsDeleted);

2. 不要过度使用过滤器 ​

csharp
// ❌ 错误: 过多的过滤器难以调试
entity.HasQueryFilter(p => !p.IsDeleted);
entity.HasQueryFilter(p => p.IsActive);
entity.HasQueryFilter(p => p.TenantId == _tenantId);
entity.HasQueryFilter(p => p.CreatedAt >= _startDate);
entity.HasQueryFilter(p => p.Category.IsValid);
// ... 太多过滤器!

// ✅ 正确: 保持简洁,复杂逻辑在查询中显式处理
entity.HasQueryFilter(p => !p.IsDeleted && p.TenantId == _tenantId);

总结 ​

过滤器决策树 ​

需要自动过滤?
│
├─ 软删除?
│  └─ ✅ HasQueryFilter(e => !e.IsDeleted)
│
├─ 多租户?
│  └─ ✅ HasQueryFilter(e => e.TenantId == currentTenantId)
│
├─ 状态过滤?
│  └─ ✅ HasQueryFilter(e => e.IsActive)
│
├─ 权限控制?
│  └─ ✅ 根据用户角色动态配置过滤器
│
└─ 时间范围?
   └─ ✅ HasQueryFilter(e => e.StartDate <= now && e.EndDate >= now)

核心要点 ​

  1. 自动应用: 无需手动添加 WHERE 条件
  2. 可覆盖: 使用 IgnoreQueryFilters() 临时禁用
  3. 组合性: 多个过滤器自动 AND 组合
  4. 性能意识: 为过滤列添加索引
  5. 谨慎使用: 避免过多过滤器导致难以调试

基于 MIT 许可发布