1

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>
Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
  • I have tried one of the cases in my project and i also was not able to send information to client while the service was at fault. I keep on getting Bad Request error. – CodeSpread Dec 02 '12 at 12:40
  • In my case I would like to know if I can avoid that exception or not. It looks like I cannot but I haven't been able to confirm it. – Daniel J.G. Dec 03 '12 at 10:22
  • I can confirm once that as i was not able to do it. I tried to send some custom message but once the call gets corrupted,it can't be overridden. – CodeSpread Dec 03 '12 at 10:26
  • Thanks, I will then assume this is the WCF behaviour and cannot be changed (Unless someone else can prove different). I have had a look at the method System.ServiceMode.Channels.ServiceChannel.ThrowIfFaultUnderstood in ildasm and it seems when you got a fault code "TransactionAborted" a ProtocolException will be thrown. – Daniel J.G. Dec 03 '12 at 14:29
  • It seems to happen only when a new transaction scope is started and aborted as part of the code executed by the service operation – Daniel J.G. Dec 05 '12 at 19:07
  • It can happen at any line of code. basically transaction will be atomic operation so same behavior. – CodeSpread Dec 06 '12 at 09:08
  • @CodeSpread, from what I can see, it only happens if I have started and aborted another transaction scope inside the service method implementation (apart from the one started by WCF). In this scenario the service implementation method ends without unhandled exceptions but internally WCF seems to notice the aborted scope and sends back a fault instead of the response returned by the service implementation. – Daniel J.G. Dec 07 '12 at 16:01
  • I can recollect from my previous experience as we were also using transaction scope and later on we had to remove it as we were told that no explicit transaction scope is required. – CodeSpread Dec 07 '12 at 17:20

0 Answers0