asp.netwcfasync-await

Context lost when IClientMessageInspector is configured on a WCF service and method is called with async/await


TLDR: does anyone knows how is context synchronization done when IClientMessageInspector is configured on a service called with await/async without ConfigureAwait(false).

We have a .NET FW4.5+ project in which we generated a proxy class using WcfUtils. In this proxy, there are Async version of the different methods.

Also, in our web.config, we have a custom (but out of our control) BehaviourExtensionElement in the system.serviceModel section which in turn will register a custom IClientMessageInspector. This inspector, when its AfterReceiveReply method is called, at some point, will use HttpContext.Current.Items[stringKey].

The problem is that even though I have no ConfigureAwait(false) anywhere in the execution path, HttpContext.Current returns null. I've been trying to log the execution and the context is present all the way up to the async call to base.Channel.MyMethod and even after since the WcfUtil proxy is generated without await (simply a return Task).

I've searched a lot on google but wasn't able to find anything specific about how continuation is done when there is a MessageInspector though I can't fathom why it would be different from the synchronization for the main call.

Edit: Used a Men in the middle inspector

I created a dummy MessageInspector that receives the messages and passes them to that is normaly configured. This allowed me to realize that ALL calls done with await don't have a context when entering the Inspector: the authenticated user is now the app pool user and HttpContext.Current is null. I also printed the stack at execution and it looks something like this.

à MyNamespace.InTheMiddleMessageInspector.AfterReceiveReply(Message& reply, Object correlationState) dans D:\Builds\sfpauw14759_1\_work\379\s\Client\AccueilClient\SX00.SXCLT00A.AccueilClient\InTheMiddleMessageInspector.cs:ligne 76
à System.ServiceModel.Dispatcher.ImmutableClientRuntime.AfterReceiveReply(ProxyRpc& rpc)
à System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
à System.ServiceModel.Channels.ServiceChannel.EndCall(String action, Object[] outs, IAsyncResult result)
à System.ServiceModel.Channels.ServiceChannelProxy.TaskCreator.<>c__DisplayClass7_0`1.<CreateGenericTask>b__0(IAsyncResult asyncResult)
à System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
à System.Threading.Tasks.TaskFactory`1.<>c__DisplayClass44_0`3.<FromAsyncImpl>b__0(IAsyncResult iar)
à System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
à System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.FinishSend(IAsyncResult result, Boolean completedSynchronously)
à System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.SendCallback(IAsyncResult result)
à System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
à System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
à System.ServiceModel.Channels.HttpChannelFactory`1.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
à System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
à System.Net.LazyAsyncResult.Complete(IntPtr userToken)
à System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
à System.Net.ContextAwareResult.Complete(IntPtr userToken)
à System.Net.LazyAsyncResult.ProtectedInvokeCallback(Object result, IntPtr userToken)
à System.Net.HttpWebRequest.ProcessResponse()
à System.Net.HttpWebRequest.SetResponse(CoreResponseData coreResponseData)
à System.Net.ConnectionReturnResult.SetResponses(ConnectionReturnResult returnResult)
à System.Net.Connection.ReadComplete(Int32 bytesRead, WebExceptionStatus errorStatus)
à System.Net.LazyAsyncResult.Complete(IntPtr userToken)
à System.Net.ContextAwareResult.Complete(IntPtr userToken)
à System.Net.LazyAsyncResult.ProtectedInvokeCallback(Object result, IntPtr userToken)
à System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
à System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)

Edit: Context is back after MessageInspector

Once we get back to the main code branch, the context is back and the stack is

à MyNamespace.MyClass.<MyMethodAsync>d__5`2.MoveNext()
à System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()
à System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
à System.Threading.Tasks.Task.Execute()
à System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
à System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
à System.Threading.Tasks.Task.ExecuteEntry(Boolean bPreventDoubleExecution)
à System.Threading.ThreadPoolWorkQueue.Dispatch()

It seems there is something wrong with how ASP.NET handles calling the MessageInspector.


Solution

  • After talking with Microsoft, this behavior is by design. I ended up with a mix of the code I made and proposed to Microsoft for review and another approach made by some analyst on their side.

    The behavior that will be referenced in the web.config:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Configuration;
    using System.ServiceModel.Description;
    using System.ServiceModel.Dispatcher;
    using System.Text;
    using System.Threading.Tasks;
    using AsyncAwareMessageInspector = AsyncAwareMessageInspectorWrapper<MyBaseInspector>;
    
        /// <summary>
        /// Wrapper pour rendre le behavior de XP compatible aux appels async
        /// </summary>
        public class AsyncAwareMessageInspectorBehavior : BehaviorExtensionElement, IEndpointBehavior
        {
            public override Type BehaviorType => typeof(AsyncAwareMessageInspectorBehavior);
    
            public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
            { }
    
            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                AddMessageInspector(endpoint, clientRuntime);
            }
    
            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
                ChannelDispatcher channelDispatcher = endpointDispatcher.ChannelDispatcher;
                if (channelDispatcher != null)
                    AjouterMessageInspector(endpoint, channelDispatcher);
            }
    
            public void Validate(ServiceEndpoint endpoint)
            { }
    
            protected virtual void AddMessageInspector(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                if (clientRuntime.MessageInspectors.All((IClientMessageInspector c) => c.GetType() != typeof(AsyncAwareMessageInspecteurWrapper)))
                    clientRuntime.MessageInspectors.Add(new AsyncAwareMessageInspector(new MyBaseInspector(endpoint)));
            }
    
            protected virtual void AjouterMessageInspector(ServiceEndpoint endpoint, ChannelDispatcher channelDispatcher)
            {
                //When a service is called, we didn't seem to lose the context so no special configuration is done and we directly use the base inspector
                var filteredEndPoints = channelDispatcher.Endpoints
                    .Where(x => x.DispatchRuntime.MessageInspectors.All((IDispatchMessageInspector c) => c.GetType() != typeof(MessageInspecteur)));
                
                foreach (EndpointDispatcher dispatcherEndpoint in filteredEndPoints)
                    dispatcherEndpoint.DispatchRuntime.MessageInspectors.Add(new MyBaseInspector(endpoint));
            }
    
            protected override object CreateBehavior()
                => new AsyncAwareMessageInspectorBehavior();
        }
    

    The inspector wrapper:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.ServiceModel.Description;
    using System.ServiceModel.Dispatcher;
    using System.ServiceModel;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.ServiceModel.Channels;
    using System.Threading;
    
        /// <typeparam name="T">Base inspector type. The main reason of using a generic is to be able to distinguish between each one in case we would have to wrap multiple base inspectors.</typeparam>
        public class AsyncAwareMessageInspectorWrapper<T> : IClientMessageInspector, IDispatchMessageInspector
            where T : IClientMessageInspector
        {
            private readonly IClientMessageInspector _innerInspector;
    
            /// <summary>
            /// If <see langword="true" /> and CorrelationState isn't available or HttpContext can't be retrieve, we throw an exception.
            /// </summary>
            private readonly bool _failIfMissing;
    
            /// <summary>
            /// If <see langword="true" /> and an exception is thrown within the base inspector, we continue. An informative text whould probably be logged somewhere.
            /// </summary>
            private readonly bool _hideThrows;
    
            public AsyncAwareMessageInspectorWrapper(T innerInspector, bool failIfMissing = false, bool hideThrows = false)
                => (_innerInspector, _failIfMissing, _hideThrows) = (innerInspector, failIfMissing, hideThrows);
    
            public void AfterReceiveReply(ref Message reply, object correlationState)
            {
                var correlation = correlationState as CorrelationStateWrapper;
    
                object innerState = correlationState;
    
                //By default, we take what is there if available
                if (correlation != null)
                {
                    innerState = correlation.InnerState;
                    HttpContext.Current ??= correlation.Context;
                }
    
                if (!_failIfMissing || HttpContext.Current != null)
                {
                    try
                    {
                        _innerInspector.AfterReceiveReply(ref reply, innerState);
                    }
                    catch (Exception ex) when (_hideThrows)
                    {
                        Trace.TraceInformation(<SomeMessageForYou>);
                    }
                }
                else
                {
                    var errMsg = "HttpContext is not available";
                    if (correlation is null)
                        throw new ArgumentNullException(nameof(correlationState), $"{errMsg} and no valid CorrelationState was defined");
                    else
                        throw new InvalidOperationException(errMsg);
                }
            }
    
            public object BeforeSendRequest(ref Message request, IClientChannel channel)
            {
                return new CorrelationStateWrapper()
                {
                    Context = HttpContext.Current, 
                    InnerState = _innerInspector.BeforeSendRequest(ref request, channel)
                };
            }
    
            public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
            {
                return (_innerInspector is IDispatchMessageInspector dispatcher)
                    ? dispatcher.AfterReceiveRequest(ref request, channel, instanceContext)
                    : null;
            }
    
            public void BeforeSendReply(ref Message reply, object correlationState)
            {
                if (_innerInspector is IDispatchMessageInspector dispatcher) 
                    dispatcher.BeforeSendReply(ref reply, correlationState);
            }
    
            private sealed class CorrelationStateWrapper
            {
                public HttpContext Context { get; set; }
                public object InnerState { get; set; }
            }
        }
    
    

    Usage in web.config

    <system.serviceModel>
      <behaviors>
        <endpointBehaviors>
          <behavior name="AsyncAware">
            <AsyncAwareMessageInspecteurBehavior />
          </behavior name="AsyncAware">
        </endpointBehaviors>
      </behaviors>
      <extensions>
        <behaviorExtensions>
          <add name="AsyncAwareMessageInspectorBehavior" type="AsyncAwareMessageInspectorBehavior, MyAssembly" />
        </behaviorExtensions>
      </extensions>
      <client>
        <endpoint address="..." binding="basicHttpBinding" bindingConfiguration="..." contract="..." name="..." behaviorConfiguration="AsyncAware" />
      </client>
    </system.serviceModel>
    

    The main difference with the MS proposition is that we really only needed the HttpContext but their solution was keeping the whole ExecutionContext which seemed overkill in our context.