2

I have a WCF service that I'm trying to make transactional. When I call the service using a transaction, the transaction is not being used and if I roll back the transaction, my database updates happen anyway.

This is the service interface (I'm only including the single method that I've been testing with:

[ServiceContract(SessionMode=SessionMode.Required)]
public interface IUserMenuPermissionService 
{
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    void InsertUserMenuPermission(string userId, string menuId, int permission);
}

The service implementation is:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class UserMenuPermissionService : IUserMenuPermissionService
{
    private static IUserMenuPermissionProvider _provider = new CoreDataFactory().GetUserMenuPermissionProvider();
    [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
    public void InsertUserMenuPermission(string userId, string menuId, int permission)
    {
        _provider.InsertUserMenuPermission(userId, menuId, permission);
    }
}

The actual underlying provider isn't doing anything with transactions directly, but at this point, that's not my issue, as will become clear in a moment.

The WCF's app.config has:

  <bindings>
      <wsHttpBinding>
          <binding name="TransactionBinding" transactionFlow="True" >
          </binding>
      </wsHttpBinding>
  </bindings>

  ...
  ...

  <service name="GEMS.Core.WCFService.UserMenuPermissionService">
    <endpoint address=""  bindingConfiguration="TransactionBinding" binding="wsHttpBinding" contract="SvcProvider.Core.WCFService.IUserMenuPermissionService">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
    <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost/SvcProvider.WebService/SvcProvider.Core.WCFService/UserMenuPermissionService/" />
      </baseAddresses>
    </host>
  </service>

The client is an ASP.NET MVC web app. It's web config has:

        <wsHttpBinding>
            <binding name="TransactionBinding" transactionFlow="true" />
        </wsHttpBinding>

        ....
        ....

        <endpoint address="http://localhost/GEMS.WebService/SvcProvider.Core.WCFService.UserMenuPermissionService.svc"
            binding="wsHttpBinding" bindingConfiguration="TransactionBinding"
            contract="UserMenuPermissionService.IUserMenuPermissionService"
            name="WsHttpBinding_IUserMenuPermissionService" />

When I call the web service from the client, I do it inside of a transaction scope. If I look at System.Transactions.Transaction.Current, it is set to a valid System.Transactions.Transaction and it has a DistributedIdentifier set.

When I'm in the WCF service, however, System.Transactions.Transaction.Current is set to null.

So it appears my transaction is not being passed along to the service. I want transactions to be optional, but when there's a transaction, obviously, I want it to be used.

Have I missed a step?

Update

As per the comments from BNL, I have now made TransactionScopeRequired = true (updated code above to reflect that).

If I call methods without a transaction scope in the client, they appear to operate just fine (I'm assuming the service creates a transaction).

If I create a transaction scope on the client and try to call the method, the first time I try to call it, it hangs for about 55 seconds and then I get a transaction aborted exception. I set the timeout for the transaction to be 3 minutes (since the default is 1 minute), but that continued to happen at about 55 seconds. The delay seems to be on the client side as it's 55 seconds after calling the autogenerated client proxy method before the WCF service actually gets called. Subsequent calls do not have the 55 second delay. The code is as follows:

using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TimeSpan(0, 3, 0)))
{
    _userMenuPermissionManager.SetPermissions(clientCode, menuPermissions);
    ts.Complete();
}

The exception happens in the disposal, not in the ts.Complete() call.

at System.Transactions.TransactionStatePromotedAborted.PromotedTransactionOutcome(InternalTransaction tx)
at System.Transactions.TransactionStatePromotedEnded.EndCommit(InternalTransaction tx)
at System.Transactions.CommittableTransaction.Commit()
at System.Transactions.TransactionScope.InternalDispose()
at System.Transactions.TransactionScope.Dispose()
at WebApp.Controllers.AdminController.SetUserMenuPermissions(String clientCode, MenuPermission[] permissions) in Controllers\\AdminController.cs:line 160

The service is being called and the transaction is being passed to it. According to the service trace log, there are no errors. Everything seems to go smoothly, but at the end, it receives an abort from the client and I guess rolls back the transaction.

The client service log shows nothing unusual that would explain the 55 second delay either.

So while I'm a bit further along (thanks BNL), I'm still not entirely there.

Update 2

I added:

[ServiceBehavior(TransactionTimeout = "00:03:00", InstanceContextMode = InstanceContextMode.PerSession)]

to the service implementation and the 55 second delay turned into a 2 minute and 55 second delay, so it appears that the transaction is timing out and THEN the service method is getting called (with the transaction) and the service does all its stuff and then the client sends an abort at the end. Subsequent calls under separate TransactionScopes abort immediately...

Update 3

It appears the timeout was being caused by an earlier call that I had missed that wasn't happening within an explicit transaction and because I don't commit transactions automatically, it was hanging because there was an uncommitted transaction. I now have all my calls wrapped in transactions. The very first call is now aborting immediately. But still no signs of any problems in the service or client trace logs. There's no inner exception, nothing. Just an abort...

Update 4

In addition to BNL's answer, apparently using TransactionAutoComplete = false is BAD. Beyond that I can't really say why it was the problem, but setting it to true fixed my issues and still allows the client to properly commit or roll back transactions (I was under the impression this was not the case with TransactionAutoComplete = true

Pete
  • 6,585
  • 5
  • 43
  • 69
  • If TransactionScopeRequired = true then the client should not be able to call without an existing transaction scope. Take a look at this answer. I put in a link and some debugging tricks. http://stackoverflow.com/questions/12398027/how-can-i-implement-wcf-transaction-support-on-custom-class-using-coreservice/12411868#12411868 – ErnieL Feb 12 '13 at 21:04
  • I had actually read your answer which is why I made the explicit comment about the DistributedIdentifier being set. If I call the service without a TransactionScope, the call will succeed, but subsequent calls inside of TransactionScopes will time out. The call being made that succeeds is actually a select (not the insert example above), but it and all the methods in the service have TransactionScopeRequired = true. – Pete Feb 12 '13 at 21:29
  • @ErnieL According to the link you sent, TransactionFlow.Mandatory requires a client-side transaction to exist, but mine is set to Allowed. I actually tried it with Mandatory to see if that fixed my problem, but it didn't... :( WCF is WAY too complex... – Pete Feb 12 '13 at 21:32

1 Answers1

2

In your second code block, you have TransactionScopeRequired = false.

If you look at the Remarks section of this document, you can determine that you won't have a transaction.

http://msdn.microsoft.com/en-us/library/system.servicemodel.operationbehaviorattribute.transactionscoperequired.aspx

TransactionScopeRequired = false

Binding permits transaction flow = true

Caller flows transaction = true

Result = Method executes without a transaction.

What is oddly not specified is what happens if both of the first two columns are true but the client doesn't flow a transaction. It looks like the service will create a new transaction, but I'm not 100% sure of that.

Community
  • 1
  • 1
BNL
  • 7,085
  • 4
  • 27
  • 32
  • 1
    Thanks. I've pored over a lot of the documentation and I guess I missed that one. They make it so cryptic and everything turns into a big matrix of possibilities. It's a little ridiculous. I guess my only solution is to have two versions, one that requires transactions and one that doesn't, as I need both possibilities. – Pete Feb 12 '13 at 18:45
  • My reading of it is that you can still not send a transaction even with that property set to true. It will just create a new one internally. I totally agree on WCF configuration being a cryptic PITA. – BNL Feb 12 '13 at 18:47
  • Found the problem. See Update 4 in the post... Thanks for your help. – Pete Feb 12 '13 at 21:45