问题描述及重现代码:
将 UnitOfWork 赋值给 BaseRepository.UnitOfWork 后执行 Commit + Dispose,Repository 上的引用不会自动清除。此后通过同一个 Repository 执行操作(如 UpdateAsync),内部会调用已 Dispose 的 UoW 的 GetOrBeginTransaction(),该方法会从连接池借出一个全新连接并开启新事务。这个新事务永远不会被提交、回滚或释放,造成孤儿事务和永久连接泄漏。
var repo = fsql.GetRepository<MyEntity>();
// 第一步:创建 UoW,绑定到 Repository,提交,释放
using (var uow = fsql.CreateUnitOfWork())
{
repo.UnitOfWork = uow;
await repo.InsertAsync(new MyEntity { Id = 1, Name = "test" });
uow.Commit();
}
// 此时 uow 已 Dispose,但 repo.UnitOfWork 仍然指向它
// 第二步:继续使用同一个 Repository(Scoped 注入场景下很常见)
repo.Attach(new MyEntity { Id = 1 });
await repo.UpdateAsync(new MyEntity { Id = 1, Name = "updated" });
// 内部静默地借出了新连接并执行了 BEGIN TRANSACTION
// 该事务不会被 Commit 或 Rollback
第二步执行后查询数据库:
- PostgreSQL:
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction'; 会看到一个孤儿连接
原因分析
三个问题叠加导致:
1. Dispose() 没有设置 Enable = false(UnitOfWork.cs)
public void Dispose()
{
if (Interlocked.Increment(ref _disposeCounter) != 1) return;
try { this.Rollback(); }
finally { GC.SuppressFinalize(this); }
// 缺少 Enable = false
}
2. GetOrBeginTransaction() 没有检查 _disposeCounter(UnitOfWork.cs)
public DbTransaction GetOrBeginTransaction(bool isCreate = true)
{
if (_tran != null) return _tran;
if (isCreate == false) return null;
if (!Enable) return null; // ← 只检查 Enable,此时仍为 true
// 缺少 if (_disposeCounter > 0) return null;
_conn = _fsql.Ado.MasterPool.Get(); // 从连接池借出新连接
_tran = _conn.Value.BeginTransaction(); // 开启新事务
DebugBeingUsed.TryAdd(this.Id, this); // 静态字典强引用,阻止 GC 回收
return _tran;
}
Commit() 之后 _tran 和 _conn 被置空,但 Enable 仍为 true,所以 GetOrBeginTransaction() 认为可以继续创建新事务。
3. RepositoryDbContext.SaveChangesSuccess() 不调用 Commit()(RepositoryDbContext.cs)
基类 DbContext.SaveChangesSuccess() 会调用 UnitOfWork?.Commit(),但 RepositoryDbContext 覆写了这个方法,只处理 EntityChangeReport,不调用 Commit。因此第二步创建的新事务没有任何代码会去提交它。
此外,DebugBeingUsed 是 static ConcurrentDictionary,持有 UoW 的强引用,导致 UoW 及其持有的连接对象永远不会被 GC 回收。
数据库版本
PostgreSQL - 16
安装的Nuget包
3.5.305
.net framework/. net core? 及具体版本
.NET 8
问题描述及重现代码:
将
UnitOfWork赋值给BaseRepository.UnitOfWork后执行 Commit + Dispose,Repository 上的引用不会自动清除。此后通过同一个 Repository 执行操作(如UpdateAsync),内部会调用已 Dispose 的 UoW 的GetOrBeginTransaction(),该方法会从连接池借出一个全新连接并开启新事务。这个新事务永远不会被提交、回滚或释放,造成孤儿事务和永久连接泄漏。第二步执行后查询数据库:
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';会看到一个孤儿连接原因分析
三个问题叠加导致:
1.
Dispose()没有设置Enable = false(UnitOfWork.cs)2.
GetOrBeginTransaction()没有检查_disposeCounter(UnitOfWork.cs)Commit()之后_tran和_conn被置空,但Enable仍为true,所以GetOrBeginTransaction()认为可以继续创建新事务。3.
RepositoryDbContext.SaveChangesSuccess()不调用Commit()(RepositoryDbContext.cs)基类
DbContext.SaveChangesSuccess()会调用UnitOfWork?.Commit(),但RepositoryDbContext覆写了这个方法,只处理EntityChangeReport,不调用 Commit。因此第二步创建的新事务没有任何代码会去提交它。此外,
DebugBeingUsed是static ConcurrentDictionary,持有 UoW 的强引用,导致 UoW 及其持有的连接对象永远不会被 GC 回收。数据库版本
PostgreSQL - 16
安装的Nuget包
3.5.305
.net framework/. net core? 及具体版本
.NET 8