在 WCF 服务上配置 IClientMessageInspector 并使用 async/await 调用方法时上下文丢失

问题描述 投票:0回答:1

TLDR:有谁知道当在没有

IClientMessageInspector
的情况下使用 wait/async 调用的服务上配置
ConfigureAwait(false)
时,上下文同步是如何完成的。

我们有一个 .NET FW4.5+ 项目,在其中我们使用 WcfUtils 生成了一个代理类。在此代理中,有不同方法的异步版本。

此外,在我们的 web.config 中,我们在 BehaviourExtensionElement 部分中有一个自定义

(但不受我们控制)
system.serviceModel
,而该部分又会注册一个自定义
IClientMessageInspector
。该检查器在调用其
AfterReceiveReply
方法时,在某些时候将使用
HttpContext.Current.Items[stringKey]

问题是,即使我在执行路径中的任何地方都没有ConfigureAwait(false),

HttpContext.Current
返回null。我一直在尝试记录执行情况,并且上下文一直存在,直到对
base.Channel.MyMethod
进行异步调用,甚至在 WcfUtil 代理生成后无需等待(只是返回任务)。

我在谷歌上搜索了很多,但无法找到有关当有 MessageInspector 时如何完成延续的任何具体信息,尽管我无法理解为什么它与主调用的同步不同。

编辑:使用了中间检查员

我创建了一个虚拟的 MessageInspector,它接收消息并将它们传递给正常配置的消息。这让我意识到,进入检查器时,所有使用 wait 完成的调用都没有上下文:经过身份验证的用户现在是应用程序池用户,并且

HttpContext.Current
为空。我还在执行时打印了堆栈,它看起来像这样。

à 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)

编辑:上下文在 MessageInspector 之后又回来了

一旦我们回到主代码分支,上下文就回来了,堆栈也变了

à 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()

ASP.NET 处理调用 MessageInspector 的方式似乎有问题。

asp.net wcf async-await
1个回答
0
投票

与 Microsoft 交谈后,此行为是设计使然的。我最终得到了我编写并建议微软审查的代码和他们方面的一些分析师提出的另一种方法的组合。

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();
    }

检查包装纸:

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; }
        }
    }

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>

与 MS 主张的主要区别在于,我们实际上只需要 HttpContext,但他们的解决方案是保留整个 ExecutionContext,这在我们的上下文中似乎有点过分了。

© www.soinside.com 2019 - 2024. All rights reserved.