Appearance
全局查询过滤器
概述
全局查询过滤器(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)核心要点
- 自动应用: 无需手动添加 WHERE 条件
- 可覆盖: 使用
IgnoreQueryFilters()临时禁用 - 组合性: 多个过滤器自动 AND 组合
- 性能意识: 为过滤列添加索引
- 谨慎使用: 避免过多过滤器导致难以调试