Dapper 的 reader 已 await using,command 为什么仍未释放
Dapper 2.1.86 包装 reader 的异步释放未处理关联 command。保留异步执行与读取,改用同步释放包装器,并分别核对资源所有权。
ExecuteReaderAsync() 返回的 reader 已放进 await using,读取也正常结束,但关联的命令没有被 Dispose。Dapper 的这个入口把 reader 和 command 包在一起;异步取得 reader,不代表包装器的两种释放路径完全相同。
Dapper 2.1.86 的 DbWrappedReader 仍有这项差异。在编译包含异步释放覆盖的目标框架上,DisposeAsync() 只转发给底层 reader;同步 Dispose() 则还会释放关联的 command。这里讨论命令所有权,不等于每个数据库驱动都会立即出现连接池耗尽。
为什么 await using 没有走完整个包装器
ExecuteReaderAsync 成功时,会把创建的 command 连同底层 reader 交给 DbWrappedReader。原方法的 finally 因此不再释放 command,后续由返回的包装器负责。
同步路径会处理 reader、将包装器标记为已释放,并处理 command。2.1.86 的异步路径却直接返回 _reader.DisposeAsync(),没有执行后两项。异步读取成功、底层 reader 被关闭,都不足以证明 command 也已经释放。
先核对使用的是 Dapper 返回的包装器,还是代码自行创建的 ADO.NET reader。若 command 是应用自己创建的,还要检查应用自己的 using 范围;不能把所有未释放命令都归到 Dapper。
在这一入口使用同步释放包装器
需要保留异步执行和读取时,可以只把包装器的释放方式改为普通 using。例如,下面的方法接收一条由调用方管理、已经打开的 DbConnection:
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
static async Task<int> ReadCountAsync(
DbConnection connection,
CancellationToken cancellationToken)
{
using DbDataReader reader = await connection.ExecuteReaderAsync(
new CommandDefinition(
"SELECT 1",
cancellationToken: cancellationToken));
int count = 0;
while (await reader.ReadAsync(cancellationToken))
{
count++;
}
return count;
}
DbConnection 类型决定这里选中返回 DbDataReader 的重载。若变量声明成 IDbConnection,另一个重载返回 IDataReader,不能原样对它调用 ReadAsync();不要为凑齐异步方法而不核对运行时类型就强制转换。
示例只说明所有权和释放范围。业务查询应保留原参数绑定、事务、超时和取消设置;传入的连接仍由调用方管理,不在这个方法中额外关闭。同步 Dispose 可能进行同步清理,不能把这个替代方式描述为“全程没有阻塞”。
必须使用异步清理的项目,也可以在 ADO.NET 层明确拥有 command 与 reader,分别管理它们的异步生命周期。那需要自行保留原来的参数、事务与执行行为,不能只把 Dapper 调用换成一条拼接 SQL。
分别观察 reader 和 command
在使用的驱动或现有测试替身中,分别记录 reader 和 command 的释放,覆盖正常读完、提前退出、读取抛错及取消。验证范围应包含包装器离开 using 的时刻,不能只看连接是否还能执行下一条 SQL。
如果连接原先由 Dapper 临时打开,还要按原来的连接所有权规则检查后续状态。不要增加第二套无条件 Close,把调用方仍需使用的连接一起关掉。
采用其他正式版本时,核对的是该版本包装器的异步释放实现及对应行为。不要仅因 ExecuteReaderAsync 的名字带 Async,便认为 await using 已覆盖其中所有资源;也不要长期依赖对私有字段的反射清理。


