为何OperationCanceledException中的CancellationToken与CTSource提供的不一致?
为什么SqlCommand取消抛出的OperationCanceledException中的CancellationToken和原Token不一致?
这个问题我之前踩过坑,咱们一步步理清楚原因和解决方案:
先看你的测试逻辑:你创建了1秒后自动取消的CancellationTokenSource,打开连接后主动等待了1秒+1秒——这时候cts.Token早就处于取消状态了,你才调用sqlQuery.ExecuteScalarAsync(cts.Token)。
问题的核心就在这里:当你把一个已经处于取消状态的CancellationToken传给SqlClient的异步方法时,SqlClient不会直接复用你传入的Token来抛出异常,而是会创建一个全新的、已取消的CancellationToken填充到OperationCanceledException的CancellationToken属性里。
为啥SqlClient要这么做?主要有两个原因:
- SqlClient内部有自己的取消逻辑封装:如果检测到传入的Token已经取消,它会直接触发快速失败,不会走实际的数据库操作流程。这时候它不会依赖外部传入的Token实例,而是生成内部的取消标记来完成异常抛出。
CancellationToken的相等性是绑定到它的源(CancellationTokenSource)的:不同源生成的Token,哪怕都是取消状态,用==或者Equals比较都会返回false。你传入的是cts生成的Token,而异常里的是SqlClient内部新创建的Token,自然不会相等。
那怎么修改测试才能让断言通过?你需要确保ExecuteScalarAsync是在执行过程中被取消,而不是一开始就传入已取消的Token。比如调整代码如下:
[Test] public static async Task SqlCommand_should_recognise_which_CT_triggered_its_cancellation() { var timeout = TimeSpan.FromSeconds(1); var cts = new CancellationTokenSource(timeout); try { var connection = new SqlConnection(_config.ConnectionString); await connection.OpenAsync(cts.Token); // 换成一个执行时间更长的查询,让cts在查询过程中触发取消 var sqlQuery = new SqlCommand("WAITFOR DELAY '00:00:02'", connection); // 直接执行查询,不需要提前等待 await sqlQuery.ExecuteScalarAsync(cts.Token); } catch (OperationCanceledException cancelledEx) { // 此时断言会成功,因为异常携带的是你传入的cts.Token Assert.AreEqual(cancelledEx.CancellationToken, cts.Token); } }
这样修改后,查询会执行2秒,而cts在1秒后触发取消,SqlClient会在查询执行过程中检测到你传入的Token被取消,这时候抛出的异常就会携带原始的cts.Token,断言自然就通过了。
内容的提问来源于stack exchange,提问作者user3284063
相关产品推荐
相关产品推荐

