How can I implement WCF Transaction support on cus

2020-04-03 15:05发布

问题:

I wrote a class to assist in adding & removing Destinations to a Publication Target using the Core Service. Destinations are normally exposed as a string (with XML content) via the Core Service, so I wrote my own wrappers around that, etc.

I now have a situation where I need to update 2 publication targets and thought it would be cool to use a transaction scope to ensure that both targets are updated at the same time.

I am however struggling with implementing this.

Code working (using standard CoreService WCF client):

TransactionOptions txOptions = new TransactionOptions 
                    { IsolationLevel = IsolationLevel.ReadCommitted };
using(TransactionScope scope = new TransactionScope(
                            TransactionScopeOption.Required, txOptions))
{
    PublicationTargetData publicationTarget1 = (PublicationTargetData)client.Read("tcm:0-1-65537", readOptions);
    PublicationTargetData publicationTarget2 = (PublicationTargetData)client.Read("tcm:0-2-65537", readOptions);

    publicationTarget1.TargetLanguage = "JSP";
    publicationTarget2.TargetLanguage = "JSP";
    client.Save(publicationTarget1, readOptions);
    client.Save(publicationTarget2, readOptions);

    // Stop saving
    scope.Dispose();
}

Executing this code will successfully roll back the changes I did (if I break before scope.Dispose() and check the publication targets in Tridion it successfully changes the target, and then "undoes" the change).

If I now try to use my "extended Publication Target" class also in a Transaction, I can't dispose it.

TransactionOptions options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    ExtendedPublicationTarget target1 = new ExtendedPublicationTarget("tcm:0-1-65537");
    ExtendedPublicationTarget target2 = new ExtendedPublicationTarget("tcm:0-2-65537");
    target1.Destinations.Add(target1.Destinations[0]);
    target2.Destinations.Add(target2.Destinations[0]);
    target1.Save();
    target2.Save();
    scope.Dispose();
}

So basically, this is the question: What must I do to add transactionality to my .Save() method?

I have tried doing this:

[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
public void Save()
{
    _client.Save(targetData, readOptions);
}

But it didn't make a difference. Is there a way to determine if I am currently in a transaction and somehow "use" that transaction? I don't want to require a transaction, just want to have the option to operate in one.

Thanks, and sorry for the very long post... wanted to make sure I provided as much info as possible.

回答1:

The best resource for this is: WCF Transaction Propagation

You are missing at least one step. You also need to enable transactions in the binding:

<bindings>
   <netTcpBinding>
      <binding name = “TransactionalTCP” transactionFlow = “true” />
   </netTcpBinding>
</bindings>

Is there a way to determine if I am currently in a transaction and somehow "use" that transaction?

Yes. To determine if you are in a transaction you can check Transaction.Current. If you are in a transaction, you will use it unless you explicitly opt out. That's the beautiful/horrible thing about ambient transactions.

Figure 5 in WCF Transaction Propagation:

class MyService : IMyContract 
{
   [OperationBehavior(TransactionScopeRequired = true)]   
   public void MyMethod(...)
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction.TransactionInformation.
                   DistributedIdentifier != Guid.Empty);
   } 
}

If Transaction.Current.TransactionInformation.DistributedIdentifier is empty, then the transaction is local and didn't "flow". Note that in a TransactionFlowOptions.Allowed configuration if the transaction fails to flow, it fails silently. So this really is the only way to check... and not flowing happens more easily than you would expect.

When I used tranactions for a production service I actually avoided TransactionFlowOptions.Allowed because the caller was never sure if the transaction actually flowed. If there was a binding configuration error in deployment, everything would run fine but rollbacks would fail... a very tendious error to detect. So I switched to required. Then a caller could ensure the transaction they provided was actually passed successfully. (If the transaction doesn't flow in a TransactionFlowOptions.Required configuration you'll get an exception.)