WCF IClientMessageInspector AfterReceiveReply主体标识错误问题
我有一个基于.NET Framework 4.6.1的WebApi应用,该应用会调用各类Web服务,因此我实现了IClientMessageInspector接口,用于记录WCF(SOAP)请求与响应的日志。
运行一段时间后我发现,部分响应日志关联的主体标识存在异常:主体A发起请求并收到响应,但该响应的日志却被记录在主体B名下。
以下为用于演示该问题的最简实现代码示例:
public class MyClientMessageInspector : IClientMessageInspector { public object BeforeSendRequest(ref Message request, IClientChannel channel) { var correlationState = new CorrelationState { Guid = Guid.NewGuid() }; // Principal A Debug.WriteLine(Thread.CurrentPrincipal); // Guid: 2abf9a7b-dac2-4c0d-b38d-1cdfb95405a7 Debug.WriteLine(correlationState.Guid); return correlationState; } public void AfterReceiveReply(ref Message reply, object correlationStateObject) { var correlationState = (CorrelationState)correlationStateObject; // Principal B - How come??? Debug.WriteLine(Thread.CurrentPrincipal); // Guid: 2abf9a7b-dac2-4c0d-b38d-1cdfb95405a7 (matches Guid from request) Debug.WriteLine(correlationState.Guid); } }
项目中Thread.CurrentPrincipal与HttpContext.Current.User仅在Global.asax.cs的Application_PostAuthenticateRequest事件中完成赋值。
该应用大量使用async/await异步编程范式,几乎所有异步调用(包括Web服务调用)后都追加了.ConfigureAwait(false)。根据已有认知,Thread.CurrentPrincipal可在线程间正常流转,不受.ConfigureAwait(false)使用的影响,其余日志(包括AfterReceiveReply执行后产生的日志)关联的主体标识均正确,也验证了这一认知。
为何AfterReceiveReply方法内的Thread.CurrentPrincipal会获取到错误的主体标识?已知可通过CorrelationState对象在BeforeSendRequest与AfterReceiveReply间传递主体标识作为临时规避方案,但希望明确该异常现象的根本原因,同时确认是否存在比该临时方案更优的解决方式。
根本原因
这个问题和ConfigureAwait(false)没有关系,核心原因是WCF客户端消息检查器的回调执行线程不参与ASP.NET异步上下文流转逻辑:
- WCF客户端的请求发送、响应接收底层走独立的IO完成端口(IOCP)线程池调度,
AfterReceiveReply回调触发时,是WCF运行时直接从CLR线程池取空闲IOCP线程执行,这个调度过程完全不感知上层ASP.NET请求上下文,既不会读取当前请求绑定的HttpContext,也不会主动把ASP.NET管道中赋值的Thread.CurrentPrincipal绑定到执行线程。 - 此时回调线程上的
Thread.CurrentPrincipal是线程池线程自带的残留值——也就是上一次使用该线程的其他请求(主体B)设置的值,自然会出现日志串主体的问题。 - 你观察到
AfterReceiveReply执行后续的日志主体又恢复正确,是因为代码从WCF回调回到你自己的await逻辑后,ASP.NET的同步上下文重新接管了线程调度,会自动把当前请求对应的正确Principal重新绑定到执行线程,不代表WCF回调阶段的上下文流转是正常的。
解决方案
你目前用correlationState传递上下文的方案根本不是临时规避,而是WCF官方推荐的跨BeforeSendRequest/AfterReceiveReply传递请求状态的标准实现,稳定性是所有方案里最高的:
- 你只需要在构建
correlationState时,除了记录Guid,同时把当前需要关联日志的Thread.CurrentPrincipal、请求TraceId等信息一并存入,在AfterReceiveReply中直接从correlationState取值做日志记录即可。这个方案完全不依赖线程上下文、逻辑调用上下文的流转,性能开销极低,也不需要兼容各种边界场景。 - 不推荐尝试用
AsyncLocal<T>、CallContext在WCF回调中传递主体,.NET Framework部分版本的WCF异步回调会主动重置逻辑调用上下文,存在随机丢上下文的风险。也不推荐写额外逻辑在AfterReceiveReply中手动读取HttpContext.Current赋值,遇到非Web场景复用WCF客户端、请求上下文提前释放的场景会直接抛出空引用异常,兼容性远差于直接用correlationState传值。
内容的提问来源于stack exchange,提问作者Anton

