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