Appearance
全局查询过滤器 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-100x2. 保持一致性
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();总结
核心要点
- 自动应用: 全局过滤器自动附加到所有查询
- 强制生效: 除非显式禁用,否则始终有效
- 典型场景: 软删除、多租户、状态过滤
- 性能优化: 为过滤器字段创建索引
- 谨慎使用: 避免过度复杂的过滤逻辑
应用场景对比
| 场景 | 过滤器复杂度 | 性能影响 | 推荐程度 |
|---|---|---|---|
| 软删除 | 简单(布尔值) | 低 | ⭐⭐⭐⭐⭐ |
| 多租户 | 简单(整数) | 低 | ⭐⭐⭐⭐⭐ |
| 状态过滤 | 中等(枚举) | 中 | ⭐⭐⭐⭐ |
| 权限过滤 | 复杂(服务依赖) | 中高 | ⭐⭐⭐ |
| 多语言 | 简单(字符串) | 低 | ⭐⭐⭐⭐ |
决策流程
是否需要自动过滤?
├─ 是 → 使用 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();