1. EF Core 全局查询筛选器深度解析
在数据访问层设计中,我们经常遇到需要自动过滤数据的场景。比如多租户系统中只返回当前租户的数据,或者软删除模式下自动过滤已删除记录。EF Core的全局查询筛选器(Global Query Filters)正是为解决这类问题而生。
1.1 筛选器的工作原理
全局查询筛选器本质上是一个LINQ表达式树,它会被EF Core自动注入到每个涉及该实体类型的LINQ查询中。当你在DbContext的OnModelCreating方法中定义如下筛选器:
csharp复制modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
EF Core会在执行查询时将其转换为类似这样的SQL:
sql复制SELECT * FROM Blogs WHERE IsDeleted = 0
这个转换发生在查询编译阶段,意味着:
- 筛选条件会被合并到原始查询中
- 不会产生额外的内存过滤开销
- 对性能几乎没有影响
1.2 高级筛选场景实践
实际项目中,我们经常需要动态的筛选条件。比如在多租户系统中:
csharp复制modelBuilder.Entity<Order>().HasQueryFilter(o =>
EF.Property<int>(o, "TenantId") == _tenantProvider.GetCurrentTenantId());
这里有几个关键点需要注意:
- 使用EF.Property访问影子属性(未在实体类中定义的属性)
- 通过服务提供器获取当前租户ID(需要先在DbContext中注入服务)
- 筛选条件会在每次查询时重新计算
重要提示:避免在筛选器中使用频繁变化的条件(如DateTime.Now),这会导致查询计划无法重用,严重影响性能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发控制机制全解
并发冲突是数据库应用的常见问题。EF Core提供了两种主要的并发控制策略:
2.1 乐观并发控制实现
乐观并发基于行版本控制,典型实现方式:
csharp复制public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
当两个用户同时编辑同一条记录时:
- 用户A读取记录(包含RowVersion=0x01)
- 用户B读取相同记录(RowVersion=0x01)
- 用户A先提交修改,RowVersion更新为0x02
- 用户B提交时,EF Core会检测到RowVersion不匹配,抛出DbUpdateConcurrencyException
处理并发异常的推荐模式:
csharp复制try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// 根据业务逻辑选择解决策略
entry.OriginalValues.SetValues(databaseValues);
// 或者
entry.CurrentValues.SetValues(databaseValues);
// 或者
throw new ConflictException("数据已被其他用户修改");
}
2.2 悲观并发控制方案
虽然EF Core不直接支持悲观锁,但可以通过原始SQL实现:
csharp复制var product = await _context.Products
.FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", id)
.FirstOrDefaultAsync();
这种方案需要注意:
- 必须在事务中使用
- 会降低系统吞吐量
- 需要合理控制锁的持有时间
3. 全局筛选器与并发控制的整合应用
3.1 软删除模式的完整实现
结合两种技术实现安全的软删除方案:
csharp复制// 实体定义
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsDeleted { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
// 配置
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
modelBuilder.Entity<Blog>().HasIndex(b => b.IsDeleted);
// 删除操作
public async Task SoftDeleteBlog(int id)
{
var blog = await _context.Blogs.IgnoreQueryFilters().FirstOrDefaultAsync(b => b.Id == id);
if (blog != null)
{
blog.IsDeleted = true;
try
{
await _context.SaveChangesAsync();
}
catch(DbUpdateConcurrencyException)
{
// 处理并发冲突
}
}
}
关键技巧:
- 使用IgnoreQueryFilters()绕过筛选器查询已删除记录
- 为IsDeleted字段创建索引提升查询性能
- 结合并发令牌防止删除操作冲突
3.2 多租户系统的完整数据隔离
csharp复制// DbContext配置
private readonly int _tenantId;
public AppDbContext(DbContextOptions options, ITenantProvider tenantProvider)
: base(options)
{
_tenantId = tenantProvider.GetCurrentTenantId();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenantId);
modelBuilder.Entity<Order>().Property(o => o.TenantId).IsRequired();
modelBuilder.Entity<Order>().HasIndex(o => o.TenantId);
}
// 查询时自动过滤租户数据
var orders = await _context.Orders.ToListAsync(); // 只会返回当前租户的订单
4. 性能优化与疑难排查
4.1 全局筛选器的性能陷阱
-
导航属性陷阱:
当查询包含被筛选实体的导航属性时,筛选器也会生效。这可能导致意外的数据丢失:csharp复制var blogs = await _context.Authors .Include(a => a.Blogs) // 这里Blogs会被自动过滤 .ToListAsync();解决方案:
- 显式使用IgnoreQueryFilters()
- 或者重构查询逻辑
-
复杂表达式性能问题:
避免在筛选器中使用:- 复杂的字符串操作
- 类型转换
- 子查询
4.2 并发冲突诊断指南
常见并发问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 频繁抛出并发异常 | 未正确处理并发令牌 | 检查实体是否正确定义了并发令牌 |
| 并发异常未被捕获 | 未配置DbContext选项 | 确保配置了.UseExceptionHandler() |
| 更新丢失但未报错 | 未使用并发控制 | 为关键实体添加并发令牌 |
4.3 高级调试技巧
-
查看生成的SQL:
csharp复制
_context.Database.Log = Console.WriteLine; -
分析查询计划:
sql复制EXPLAIN SELECT * FROM Blogs WHERE IsDeleted = 0 -
监控锁竞争:
sql复制SELECT * FROM sys.dm_tran_locks WHERE resource_database_id = DB_ID()
5. 实战经验与架构建议
在实际企业级应用中,我有以下几点经验分享:
-
筛选器的最佳实践:
- 为所有软删除实体添加[IsDeleted]索引
- 避免在筛选器中使用服务定位器(破坏DbContext的可测试性)
- 考虑将租户ID等常用筛选条件抽象为基类
-
并发控制的黄金法则:
- 关键业务实体必须实现并发控制
- 更新操作必须处理并发异常
- 前端应实现乐观并发提示界面
-
性能权衡建议:
- 读多写少场景用乐观并发
- 写密集型操作考虑悲观锁
- 全局筛选器对性能影响较小,但要注意表达式复杂度
-
测试策略:
csharp复制[Fact] public async Task Should_Filter_Deleted_Items() { // 准备 var deletedBlog = new Blog { IsDeleted = true }; _context.Blogs.Add(deletedBlog); await _context.SaveChangesAsync(); // 执行 var blogs = await _context.Blogs.ToListAsync(); // 断言 Assert.DoesNotContain(blogs, b => b.IsDeleted); } -
架构设计提示:
- 将全局筛选逻辑封装在DbContext基类中
- 为不同的业务场景创建不同的DbContext派生类
- 考虑使用CQRS模式分离读写操作
