DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
晚安云;香港 CN2 ¥15/月

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 已覆盖其中所有资源;也不要长期依赖对私有字段的反射清理。