.NET Core CAP同步订阅者抛异常且捕获块未触发原因问询
核心原因:未等待异步方法导致的异常逃逸
你的同步订阅者方法里犯了一个典型的异步编程错误——调用异步方法但没有使用await等待其完成。
在你的同步Handle方法中:
try { _recalcProcessor.RecalcByPolicyAsync(integrationEvent.RecalculationDate, integrationEvent.EmployeeId, integrationEvent.PolicyId); } catch (Exception ex) { // 永远进不来,因为异常没在当前上下文抛出 }
这行代码会启动RecalcByPolicyAsync异步操作,但不会等待它完成,Handle方法会直接执行到末尾并返回。而异步操作后续抛出的异常属于未观察到的Task异常,这个异常不会冒泡到当前的try/catch块,反而会在Task被垃圾回收时触发.NET的未观察任务异常机制(旧版本中会直接终止进程,新版本虽有改进,但CAP的同步订阅上下文没有处理这类异常的逻辑)。
你看到的Invalid attempt to call FieldCount when reader is closed异常,本质是因为RecalcByPolicyAsync里的using (var uow = _uowFactory.Create(false))块在异步方法还没执行完时就被释放了——Handle方法已经返回,uow的生命周期结束,数据库连接被关闭,但此时Connection.QueryAsync还在尝试读取数据,所以抛出这个错误。
为什么异步订阅者能正常工作?
当Handle方法是异步签名(async Task)时,你必然会使用await调用异步方法:
await _recalcProcessor.RecalcByPolicyAsync(...);
await会暂停当前方法的执行,直到异步操作完成,此时异步操作抛出的异常会被正确冒泡到当前的try/catch块,同时using块也会等到异步操作完成后才释放资源,不会出现连接提前关闭的问题。
针对你的需求的解决方案
你担心异步处理会导致SQL连接耗尽,但实际上异步IO反而能更高效地利用连接池(线程不会在等待IO时被阻塞)。不过如果你确实需要同步处理消息,这里有两种可靠的解决方式:
1. 在同步方法中正确等待异步操作
使用.GetAwaiter().GetResult()来等待异步方法完成(比.Wait()更友好,不会包裹AggregateException),这样异常会被当前的try/catch捕获,同时using块也会等到操作完成后释放:
[CapSubscribe(nameof(IntegrationEvent))] public void Handle(IntegrationEvent integrationEvent) { _logger.LogInformation("some message"); try { _recalcProcessor.RecalcByPolicyAsync( integrationEvent.RecalculationDate, integrationEvent.EmployeeId, integrationEvent.PolicyId) .GetAwaiter() .GetResult(); } catch (Exception ex) { _logger.LogError(ex, "some message"); } }
2. 调整CAP的全局异常处理策略(辅助方案)
可以配置CAP的全局异常处理来捕获这类未观察到的异常,但这只是兜底手段,核心还是要正确处理异步调用的等待:
services.AddCap(x => { // 其他配置... x.FailedThresholdCallback = (ex) => { // 全局处理未捕获的异常 _logger.LogError(ex, "CAP消息处理失败"); }; });
关于连接耗尽的补充说明
如果你担心异步处理导致连接池耗尽,建议检查:
- 连接池的
Max Pool Size配置是否合理 - 是否有其他地方没有正确释放数据库连接
- 异步处理本身不会消耗更多连接,反而能减少线程阻塞,提高连接的利用率
内容的提问来源于stack exchange,提问作者Bob Horn

