dotnetcore / dotnetcore/FreeSql
UnitOfWork 在 Dispose 后仍能创建新事务,导致孤儿事务和连接泄漏
- Dominant language
- C#
- Stars
- 4.4k
- Forks
- 910
- PR merge metrics
- No merged PRs in 30d
Description
#### 问题描述及重现代码:
将 `UnitOfWork` 赋值给 `BaseRepository.UnitOfWork` 后执行 Commit + Dispose,Repository 上的引用不会自动清除。此后通过同一个 Repository 执行操作(如 `UpdateAsync`),内部会调用已 Dispose 的 UoW 的 `GetOrBeginTransaction()`,**该方法会从连接池借出一个全新连接并开启新事务**。这个新事务永远不会被提交、回滚或释放,造成孤儿事务和永久连接泄漏。
```c#
var repo = fsql.GetRepository();
// 第一步:创建 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](https://github.com/dotnetcore/FreeSql/blob/master/FreeSql.DbContext/UnitOfWork/UnitOfWork.cs))
```csharp
public void Dispose()
{
if (Interlocked.Increment(ref _disposeCounter) != 1) return;
try { this.Rollback(); }
finally { GC.SuppressFinalize(this); }
// 缺少 Enable = false
}
```
**2. `GetOrBeginTransaction()` 没有检查 `_disposeCounter`**([UnitOfWork.cs](https://github.com/dotnetcore/FreeSql/blob/master/FreeSql.DbContext/UnitOfWork/UnitOfWork.cs))
```csharp
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](https://github.com/dotnetcore/FreeSql/blob/master/FreeSql.DbContext/Repository/ContextSet/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
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with FreeSql.DbContext/UnitOfWork/UnitOfWork.cs, especially Dispose() and GetOrBeginTransaction(), then inspect FreeSql.DbContext/Repository/ContextSet/RepositoryDbContext.cs and its SaveChangesSuccess() override. Reproduce the scoped-repository sequence from the issue and verify that using a repository after disposal cannot create an untracked transaction or leak a connection; add regression coverage where the project’s existing tests belong.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, postgresql
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100