DbContext缓存提交数据的原因及Blazor Server下EF Core异常问题咨询
先帮你拆解遇到的几个核心问题,以及背后的底层原因:
1. 为什么user1会被重复插入,db.User.Local.Count还持续增加?
核心问题出在服务生命周期不匹配:
- 你把
IUserRepository注册成了Singleton(单例),这意味着整个应用生命周期里只有一个UserRepository实例。 - 而
AppContext(DbContext)注册成了Transient,但因为仓储是单例,它构造函数里注入的AppContext会被永久持有——第一次注入的那个DbContext会一直被复用,不会每次操作都创建新的。
当第一次添加user1失败时,这个实体已经被DbContext的ChangeTracker标记为Added状态并跟踪了。即使你后来尝试添加user2,调用SaveChangesAsync时,ChangeTracker里所有被跟踪的实体(包括失败的user1)都会被一起提交到数据库,导致重复插入。db.User.Local.Count持续增加,正是因为这些失败的实体一直被ChangeTracker缓存着。
另外,你的AddAsync方法吞了异常(catch (Exception e) { }),这会导致调用方完全不知道操作失败,无法及时处理错误状态,进一步加剧了问题。
2. 为什么编辑后查询仍显示旧数据?
这是EF Core ChangeTracker的默认优化行为:它会缓存已经查询过的实体实例。当你第一次查询某个用户后,ChangeTracker会保留这个实例;后续再查询同一用户时,EF Core会直接从缓存返回这个实例,而不是去数据库拉取最新数据——这是为了减少数据库查询次数,提升应用性能。
3. DbContext为什么会缓存提交的数据?
EF Core的ChangeTracker是其核心组件,它的主要作用是:
- 跟踪实体的状态变化(新增、修改、删除)
- 生成高效的SQL语句,只对真正变化的字段执行更新
- 避免重复加载相同实体,降低数据库压力
缓存(跟踪)实体是实现这些功能的基础——只有当实体被跟踪时,EF Core才能准确识别哪些字段被修改了,从而生成精准的更新语句。
解决方案
调整服务生命周期(最关键)
Blazor Server中,每个SignalR连接对应一个Scoped生命周期范围。你应该把仓储和DbContext都注册为Scoped:
// Startup.cs 或 Program.cs(.NET 6+) services.AddScoped<IUserRepository, UserRepository>(); services.AddDbContext<AppContext>(options => options.UseSqlServer("你的数据库连接字符串")); // 注:AddDbContext默认就是Scoped生命周期,无需额外指定Transient
这样每个用户连接会有独立的仓储和DbContext实例,操作之间不会互相干扰,ChangeTracker的状态也不会跨请求残留。
修复UserRepository的代码问题
不要吞异常,并且确保异步方法正确使用await:
public class UserRepository : IUserRepository { private readonly AppContext _db; public UserRepository(AppContext context) { _db = context; } public async Task AddAsync(User user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); try { await _db.User.AddAsync(user, cancellationToken); await _db.SaveChangesAsync(cancellationToken); } catch (Exception e) { // 将失败的实体从ChangeTracker中移除,避免后续提交重复插入 _db.Entry(user).State = EntityState.Detached; // 不要吞异常,向上抛出让调用方处理错误 throw; } } }
获取最新数据的几种方式
如果需要绕过缓存获取数据库最新状态,可以用以下方法:
- 查询时禁用跟踪:适合只读场景
var latestUser = await _db.User.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId); - 刷新已跟踪的实体:如果已有缓存实体,强制从数据库重新加载
var existingUser = await _db.User.FindAsync(userId); await _db.Entry(existingUser).ReloadAsync(); - 清空ChangeTracker:一次性清除所有缓存实体(谨慎使用,会丢失未提交的状态)
_db.ChangeTracker.Clear();
内容的提问来源于stack exchange,提问作者reza malekmohamadi

