dotnetcore / dotnetcore/FreeSql

UnitOfWork 在 Dispose 后仍能创建新事务,导致孤儿事务和连接泄漏

Open
#2,226 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.