Will a WCF client throw a protocol exception when

2019-07-26 05:14发布

问题:

I have an existing web service where transaction flow was not supported and returns errors in an error collection of the response rather than throwing faults. So the response message contract looks like this:

[MessageContract()]
public class UpdateResponse
{
    [MessageBodyMember()]
    public UpdateData Data { get; set; }

    [MessageBodyMember()]
    public ErrorMessages Errors { get; set; }        
}

Now this service will be updated so its operations can be included in a distributed transaction, which will be started on the client side. As it is not throwing faults, I have enabled transaction flow and set transactionAutoComplete as false.

So in the operation implementation I will manually commit the transaction only when the collection of errors is empty:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
            ConcurrencyMode = ConcurrencyMode.Single,
            TransactionIsolationLevel = IsolationLevel.ReadCommitted)]
public partial class MyService: MyServiceContract
{
    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    public UpdateResponse Update(UpdateRequest request)
    {
        UpdatePlanResponse response = new UpdateResponse();

        ... call inner components to perform operation ...

        if (errors.Count == 0)
        {
            OperationContext.Current.SetTransactionComplete();
        }
        return response;
    }
}

On the client side, I have also enabled the transaction flow in the wsHttpbinding configuration. The service is being called inside a transaction scope like this:

    using (TransactionScope tran = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{ IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted}))
    {
        UpdateResponse response = _serviceClient.Update(updateRequest);

        ... some more work ...

        if(response.Errors != null && response.Errors.Count > 0)
        {
            ... handle and raise them ...
        }
        tran.Complete();
    }

The transaction flows to the service. In the positive scenario all works as expected, and changes made by the service code are only persisted when the client commits the transaction. Now if any issues are found on the service, the service will abort the transaction (because an inner transaction scope has been created and in that scenario it will not be completed) and a response message with a number of errors in the errors collection will be send back.

However on the client side, a ProtocolException stating "The transaction under which this method call was executing was asynchronously aborted." is thrown by WCF:

Server stack trace: at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter) at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc) at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout) at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation) at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)

Exception rethrown at [0]: at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg) at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type) at MyServiceContract.Update(UpdateRequest request) at MyServiceContractClient.Update(UpdateRequest request)

Is there anything I can do in this scenario, so I can abort the transaction on the service side while still sending some information to the client about what went wrong without throwing a fault?

Throwing faults would make more sense here (and avoids me enabling sessions, which is needed for setting transaction auto complete as false), but as it is an existing service I wanted to check my options before using them.

UPDATE

If the service operation is implemented like in the following example, the response including the error information will reach the client. I can see the transaction status as aborted but no ProtocolException is thrown by WCF:

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    public UpdateResponse Update(UpdateRequest request)
    {
        UpdatePlanResponse response = new UpdateResponse();

        response.errors = new List<Error>() { new Error("dummy"); }
        if (response.errors.Count == 0)
        {
            OperationContext.Current.SetTransactionComplete();
        }
        return response;
    }

However if I start another transaction scope on the service implementation and this gets aborted, then a fault will be send back to the client where a ProtocolException will be raised:

    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    public UpdateResponse Update(UpdateRequest request)
    {
        UpdatePlanResponse response = new UpdateResponse();

        //start another transaction scope and abort it
        using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
        {
        }

        response.errors = new List<Error>() { new Error("dummy"); }
        if (response.errors.Count == 0)
        {
            OperationContext.Current.SetTransactionComplete();
        }
        return response;
    }

It seems as in the second case, WCF tries to abort a transaction that was already aborted and this results in the following fault being send back to client...

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:r="http://schemas.xmlsoap.org/ws/2005/02/rm" xmlns:a="http://www.w3.org/2005/08/addressing">
  <s:Header>
     ...
  </s:Header>
  <s:Body>
    <s:Fault>
      <s:Code>
        <s:Value>s:Sender</s:Value>
        <s:Subcode>
          <s:Value xmlns:a="http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher">a:TransactionAborted</s:Value>
        </s:Subcode>
      </s:Code>
      <s:Reason>
        <s:Text xml:lang="en-GB">The transaction under which this method call was executing was asynchronously aborted.</s:Text>
      </s:Reason>
    </s:Fault>
  </s:Body>
</s:Envelope>

Enabling tracing I can see the following exception being logged for System.ServiceModel source in the service side (Due to this exception, the fault above is sent to the client):

<TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Error">
    <TraceIdentifier>http://msdn.microsoft.com/en-GB/library/System.ServiceModel.Diagnostics.TraceHandledException.aspx</TraceIdentifier>
    <Description>Handling an exception.</Description>
    <AppDomain>a0ef2bea-25-129990861717638246</AppDomain>
    <Exception>
        <ExceptionType>System.ServiceModel.FaultException, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
        <Message>The transaction under which this method call was executing was asynchronously aborted.</Message>
        <StackTrace>
            at System.ServiceModel.Diagnostics.ExceptionUtility.TraceHandledException(Exception exception, TraceEventType eventType)
            at System.ServiceModel.Dispatcher.TransactionInstanceContextFacet.CheckIfTxCompletedAndUpdateAttached(MessageRpc&amp; rpc, Boolean isConcurrent)
            at System.ServiceModel.Dispatcher.TransactionBehavior.ResolveOutcome(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ResolveTransactionOutcome(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage9(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage8(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage41(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage11(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&amp; rpc)
            at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
            at System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContext request, Boolean cleanThread, OperationContext currentOperationContext)
            at System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContext request, OperationContext currentOperationContext)
            at System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResult result)
            at System.ServiceModel.Dispatcher.ChannelHandler.OnAsyncReceiveComplete(IAsyncResult result)
            at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
            at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
            at System.ServiceModel.Channels.ReceiveTimeoutAsyncResult.Callback(IAsyncResult result)
            at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
            at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
            at System.Runtime.InputQueue`1.AsyncQueueReader.Set(Item item)
            at System.Runtime.InputQueue`1.Dispatch()
            at System.ServiceModel.Channels.ReliableReplySessionChannel.ProcessSequencedMessage(RequestContext context, String action, WsrmSequencedMessageInfo info)
            at System.ServiceModel.Channels.ReliableReplySessionChannel.ProcessRequest(RequestContext context, WsrmMessageInfo info)
            at System.ServiceModel.Channels.ReliableReplySessionChannel.ProcessDemuxedRequest(RequestContext context, WsrmMessageInfo info)
            at System.ServiceModel.Channels.ReliableReplyListenerOverReply.ProcessSequencedItem(ReliableReplySessionChannel reliableChannel, RequestContext context, WsrmMessageInfo info)
            at System.ServiceModel.Channels.ReliableListenerOverDatagram`4.HandleReceiveComplete(TItem item, TInnerChannel channel)
            at System.ServiceModel.Channels.ReliableListenerOverDatagram`4.OnTryReceiveComplete(IAsyncResult result)
            at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
            at System.ServiceModel.Diagnostics.TraceUtility.&lt;&gt;c__DisplayClass4.&lt;CallbackGenerator&gt;b__2(AsyncCallback callback, IAsyncResult result)
            at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
            at System.Runtime.InputQueue`1.AsyncQueueReader.Set(Item item)
            at System.Runtime.InputQueue`1.EnqueueAndDispatch(Item item, Boolean canDispatchOnThisThread)
            at System.Runtime.InputQueue`1.EnqueueAndDispatch(T item, Action dequeuedCallback, Boolean canDispatchOnThisThread)
            at System.ServiceModel.Channels.SingletonChannelAcceptor`3.Enqueue(QueueItemType item, Action dequeuedCallback, Boolean canDispatchOnThisThread)
            at System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContext context, Action callback)
            at System.ServiceModel.Activation.HostedHttpTransportManager.HttpContextReceived(HostedHttpRequestAsyncResult result)
            at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.HandleRequest()
            at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.BeginRequest()
            at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.OnBeginRequest(Object state)
            at System.Runtime.IOThreadScheduler.ScheduledOverlapped.IOCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
            at System.Runtime.Fx.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
            at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
        </StackTrace>
        <ExceptionString>System.ServiceModel.FaultException: The transaction under which this method call was executing was asynchronously aborted.</ExceptionString>
    </Exception>
</TraceRecord>