Section 7.7. Explicit Transaction Programming


7.7. Explicit Transaction Programming

The transactional programming model described so far can only be used declaratively by transactional services. Nonservice clients, nontransactional services, or just plain .NET objects called downstream by a service cannot take advantage of it. For all these cases, WCF relies on the transactional infrastructure available with .NET 2.0 in the System.Transactions namespace. In addition, you may rely on System.Transactions even in transactional services when exploiting some advanced features such as transaction events, cloning, asynchronous commit, and manual transactions. I described the System.Transactions capabilities in my MSDN whitepaper "Introducing System.Transactions in the .NET Framework 2.0" (published April 2005; updated December 2005). The flowing sections contain excerpts from that article describing how to use the core aspects of System.Transactions in the context of WCF. Please refer to the whitepaper for detailed discussions of the rest of the features.

7.7.1. The TransactionScope Class

The most common way of using transactions explicitly is via the TRansactionScope class:

 public class TransactionScope : IDisposable {    public TransactionScope( );    //Additional constructors    public void Complete( );    public void Dispose( ); } 

As the name implies, the transactionScope class is used to scope a code section with a transaction, as demonstrated in Example 7-7.

Example 7-7. Using TransactionScope

 using(TransactionScope scope = new TransactionScope( )) {    /* Perform transactional work here */    //No errors - commit transaction    scope.Complete( ); } 

The scope constructor can create a new LTM transaction and make it the ambient transaction by setting transaction.Current, or can join an existing ambient transaction. TRansactionScope is a disposable objectif the scope creates a new transaction, the transaction will end once the Dispose( ) method is called (the end of the using statement in Example 7-7). The Dispose( ) method also restores the original ambient transaction (null in the case of Example 7-7).

Finally, if the transactionScope object is not used inside a using statement, it would become garbage once the transaction timeout is expired and the transaction is aborted.

7.7.1.1. TransactionScope voting

The TRansactionScope object has no way of knowing whether the transaction should commit or abort. To address this, every transactionScope object has a consistency bit, which is by default is set to false. You can set the consistency bit to true by calling the Complete( ) method. Note that you can only call Complete( ) once. Subsequent calls to Complete( ) will raise an InvalidOperationException. This is deliberate, to encourage developers to have no transactional code after the call to Complete( ).

If the transaction ends (due to calling Dispose( ) or garbage collection) and the consistency bit is set to false, the transaction will abort. For example, the following scope object will abort its transaction, because the consistency bit is never changed from its default value:

 using(TransactionScope scope = new TransactionScope( )) {} 

By having the call to Complete( ) as the last action in the scope, you have an automated way for voting to abort in case of an error. The reason is that any exception thrown inside the scope will skip over the call to Complete( ); the finally statement in the using statement will dispose of the transactionScope object; and the transaction will abort. On the other hand, if you do call Complete( ) and the transaction ends with the consistency bit set to TRue as in Example 7-7, the transaction will try to commit. Note that after calling Complete( ), you cannot access the ambient transaction, and trying to do so will result in an InvalidOperationException. You can access the ambient transaction (via transaction.Current) again once the scope object is disposed of.

The fact that the code in the scope called Complete( ) does not guarantee committing the transaction. Even if you call Complete( ) and the scope is disposed of, all that will do is try to commit the transaction. The ultimate success or failure of that attempt is the product of the two-phase commit protocol, which may involve multiple resources and services your code is unaware of. As a result, Dispose( ) will throw transactionAbortedException if it fails to commit the transaction. You can catch and handle that exception, perhaps by alerting the user, as shown in Example 7-8.

Example 7-8. TransactionScope and error handling

 try {    using(TransactionScope scope = new TransactionScope( ))    {       /* Perform transactional work here */       //No errors - commit transaction       scope.Complete( );    } } catch(TransactionAbortedException e) {    Trace.Writeline(e.Message); } catch //Any other exception took place {    Trace.Writeline("Cannot complete transaction");    throw; } 

7.7.2. Transaction Flow Management

Transaction scopes can nest both directly and indirectly. In Example 7-9, scope2 simply nests inside scope1.

Example 7-9. Direct scope nesting

 using(TransactionScope scope1 = new TransactionScope( )) {    using(TransactionScope scope2 = new TransactionScope( ))    {       scope2.Complete( );    }    scope1.Complete( ); } 

The scope can also nest indirectly when calling a method that uses transactionScope from within a method that uses its own scope, as is the case with the RootMethod( ) in Example 7-10.

Example 7-10. Indirect scope nesting

 void RootMethod( ) {    using(TransactionScope scope = new TransactionScope( ))    {       /* Perform transactional work here */       SomeMethod( );       scope.Complete( );    } } void SomeMethod( ) {    using(TransactionScope scope = new TransactionScope( ))    {       /* Perform transactional work here */       scope.Complete( );    } } 

A transaction scope can also nest in a service method, as in Example 7-11. The service method may or may not be transactional.

Example 7-11. Scope nesting inside a service method

 class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {       using(TransactionScope scope = new TransactionScope( ))       {          scope.Complete( );       }    } } 

If the scope creates a new transaction for its use, it is called the root scope. Whether or not a scope becomes a root scope depends on the scope configuration and the presence of an ambient transaction. Once a root scope is established, there is an implicit relationship between it and all its nested scopes or downstream services called.

The transactionScope class provides several overloaded constructors that accept an enum of the type transactionScopeOption:

 public enum TransactionScopeOption {    Required,    RequiresNew,    Suppress } public class TransactionScope : IDisposable {    public TransactionScope(TransactionScopeOption scopeOption);    public TransactionScope(TransactionScopeOption scopeOption,                            TransactionOptions transactionOptions);    public TransactionScope(TransactionScopeOption scopeOption,                            TimeSpan scopeTimeout);    //Additional constructors and memebrs } 

The value of transactionScopeOption lets you control whether the scope takes part in a transaction and, if so, whether it will join the ambient transaction or will be the root scope of a new transaction.

For example, here is how you specify the value of the transactionScopeOption in the scope's constructor:

 using(TransactionScope scope                            = new TransactionScope(TransactionScopeOption.Required)) {...} 

The default value for the scope option is transactionScopeOption.Required, meaning this is the value used when you call one of the constructors that does not accept a transactionScopeOption parameter, so these two definitions are equivalent:

 using(TransactionScope scope = new TransactionScope( )) {...} using(TransactionScope scope                            = new TransactionScope(TransactionScopeOption.Required)) {...} 

The TRansactionScope object determines which transaction to belong to when it is constructed. Once determined, the scope will always belong to that transaction. transactionScope bases its decision on two factors: whether an ambient transaction is present, and the value of the transactionScopeOption parameter.

A TRansactionScope object has three options:

  • Join the ambient transaction

  • Be a new scope root; that is, start a new transaction and have that transaction be the new ambient transaction inside its own scope

  • Not take part in a transaction at all

If the scope is configured with transactionScopeOption.Required, and an ambient transaction is present, the scope will join that transaction. If, on the other hand, there is no ambient transaction, then the scope will create a new transaction and become the root scope.

If the scope is configured with transactionScopeOption.RequiresNew, then it will always be a root scope. It will start a new transaction, and its transaction will be the new ambient transaction inside the scope.

If the scope is configured with TRansactionScopeOption.Suppress it will never be part of a transaction, regardless of whether an ambient transaction is present. A scope configured with transactionScopeOption.Suppress will always have null as its ambient transaction.

7.7.2.1. Voting inside a nested scope

It is important to realize that although a nested scope can join the ambient transaction of its parent scope, the two scope objects will have two distinct consistency bits. Calling Complete( ) in the nested scope has no effect on the parent scope:

 using(TransactionScope scope1 = new TransactionScope( )) {    using(TransactionScope scope2 = new TransactionScope( ))    {       scope2.Complete( );    }    //scope1's consistency bit is still false } 

Only if all the scopes, from the root scope down to the last nested scope, vote to commit the transaction will the transaction commit. In addition, only the root scope dictates the life span of the transaction. When a transactionScope object joins an ambient transaction, disposing of that scope does not end the transaction. The transaction ends only when the root scope is disposed, or when the service method that started the transaction returns.

7.7.2.2. TransactionScopeOption.Required

transactionScopeOption.Required is not just the most common value used; it is also the most decoupled value. If your scope has an ambient transaction, it will join the ambient transaction to improve consistency. However, if it cannot, the scope will at least provide the code with a new ambient transaction. When TRansactionScopeOption.Required is used, the code inside the transactionScope must not behave differently when it is the root or when it is just joining the ambient transaction. It should operate identically in both cases. On the service side, the most common use for transactionScopeOption.Required is by nonservice downstream classes called by the service, as shown in Example 7-12.

Example 7-12. Using TransactionScopeOption.Required in a downstream class

 class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {       MyClass obj = new MyClass( );       obj.SomeMethod( );    } } class MyClass {    public void SomeMethod( )    {       using(TransactionScope scope = new TransactionScope( ))       {          //Do some work then          scope.Complete( );       }    } } 

While the service itself can use TRansactionScopeOption.Required directly, such practice adds no value:

 class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {       //One transaction only       using(TransactionScope scope = new TransactionScope( ))       {          //Do some work then          scope.Complete( );       }    } } 

The reason is obvious: the service can simply ask WCF to scope the operation with a transaction scope by setting transactionScopeRequired to TRue (this is also the origin of that property's name). Note that even though the service may use declarative voting, any downstream (or directly nested) scope must still explicitly call Complete( ) in order for the transaction to commit.

The same is true when the service method uses explicit voting:

 [OperationBehavior(TransactionScopeRequired = true,                    TransactionAutoComplete = false)] public void MyMethod(...) {    using(TransactionScope scope = new TransactionScope( ))    {       //Do some work then       scope.Complete( );    }    /* Do transactional work here, then: */    OperationContext.Current.SetTransactionComplete( ); } 

In short, voting to abort in a scope with transactionScopeRequired nested in a service call will abort the service transaction regardless of exceptions or the use of declarative voting (via TRansactionAutoComplete) or explicit voting by the service (via SetTransactionComplete( )).

7.7.2.3. TransactionScopeOption.RequiresNew

Configuring the scope with TRansactionScopeOption.RequiresNew is useful when you want to perform transactional work outside the scope of the ambient transaction; for example, when you want to perform some logging or audit operations, or when you want to publish events to subscribers, regardless of whether your ambient transaction commits or aborts:

 class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {       //Two distinct transactions       using(TransactionScope scope =                           new TransactionScope(TransactionScopeOption.RequiresNew))       {          //Do some work then          scope.Complete( );       }    } } 

Note that you must complete the scope in order for the new transaction to commit. You may also want to consider encasing a scope that uses TRansactionScopeOption.RequiresNew in a TRy and catch statement to isolate it from the service's ambient transaction.

You should be extremely careful when using transactionScopeOption.RequiresNew and verify that the two transactions (the ambient transaction and the one created for your scope) do not jeopardize consistency if one aborts and the other commits.

7.7.2.4. TransactionScopeOption.Suppress

transactionScopeOption.Suppress is useful for both the client and the service when the operations performed by the code section are nice to have and should not abort the ambient transaction if the operations fail. transactionScopeOption.Suppress allows you to have a nontransactional code section inside a transactional scope or service operation, as shown in Example 7-13.

Example 7-13. Using TransactionScopeOption.Suppress

 [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod(...) {    try    {       //Start of nontransactional section       using(TransactionScope scope = new                                  TransactionScope(TransactionScopeOption.Suppress))       {          //Do nontransactional work here       }//Restores ambient transaction here    }    catch    {} } 

Note in Example 7-13 that there is no need to call Complete( ) on the suppressed scope. Another example where transactionScopeOption.Suppress is useful is when you want to provide some custom behavior and you need to perform your own programmatic transaction support or manually enlist resources.

That said, you should be careful when mixing transactional scopes or service methods with nontransactional scopes, as that can jeopardize isolation and consistency, because changes made to the system state inside the suppressed scope will not roll back along with the containing ambient transaction. In addition, the nontransactional scope may have errors, but those errors should not affect the ambient transaction outcome. This is why in Example 7-13 the suppressed scope is encased in a try and catch statement that also suppresses any exception coming out of it.

Do not call a service configured for Client transactions (basically with mandatory transaction flow) inside a suppressed scope, because that call is guaranteed to fail.


7.7.2.5. TransactionScope timeout

If the code inside the transactional scope takes a long time to complete, it may be indicative of a transactional deadlock. To address that, the transaction will automatically abort if executed for more than a predetermined timeout (60 seconds by default). You can configure the default timeout in the application config file. For example, to configure a default timeout of 30 seconds, add this to the config file:

 <system.transactions>    <defaultSettings timeout = "00:00:30"/> </system.transactions> 

Placing the new default in the application config file affects all scopes used by all clients and services in that application. You can also configure a timeout for a specific transaction scope. A few of the overloaded constructors of TRansactionScope accept a value of type TimeSpan, used to control the timeout of the transaction, for example:

 public TransactionScope(TransactionScopeOption scopeOption,                         TimeSpan scopeTimeout); 

To specify a timeout different from the default of 60 seconds, simply pass in the desired value:

 TimeSpan timeout = TimeSpan.FromSeconds(30); using(TransactionScope scope                    = new TransactionScope(TransactionScopeOption.Required,timeout)) {...} 

When a TRansactionScope joins the ambient transaction, yet specifies a shorter timeout than the one the ambient transaction is set to, it has the effect of enforcing the new, shorter timeout on the ambient transaction, and the transaction must end within the nested time specified, or it is automatically aborted. If the scope's timeout is greater than that of the ambient transaction, it has no effect.

7.7.2.6. TransactionScope isolation level

If the scope is a root scope, by default the transaction will execute with the isolation level set to serializable. Some of the overloaded constructors of transactionScope accept a structure of the type transactionOptions, defined as:

 public struct TransactionOptions {    public IsolationLevel IsolationLevel    {get;set;}    public TimeSpan Timeout    {get;set;}    //Other members } 

Although you can use the transactionOptions Timeout property to specify a timeout, the main use for TRansactionOptions is for specifying isolation level. You could assign into TRansactionOptions IsolationLevel property a value of the enum type IsolationLevel presented earlier:

 TransactionOptions options = new TransactionOptions( ); options.IsolationLevel = IsolationLevel.ReadCommitted; options.Timeout = TransactionManager.DefaultTimeout; using(TransactionScope scope                    = new TransactionScope(TransactionScopeOption.Required,options)) {...} 

When a scope joins an ambient transaction, it must be configured to use exactly the same isolation level as the ambient transaction, otherwise an ArgumentException is thrown.

7.7.3. Nonservice Clients

Although services can take advantage of transactionScope, by far its primary use is by nonservice clients. Using a transaction scope is practically the only way a nonservice client can group multiple service calls into single transaction, as shown in Figure 7-7.

Figure 7-7. A nonservice client using a single transaction to call multiple services


Having the option to create a root transaction scope enables the client to flow its transaction to services and to manage and commit the transaction based on the aggregated result of the services, as shown in Example 7-14.

Example 7-14. Using TransactionScope to call services in a single transaction

 ////////////////////////// Service Side //////////////////////////// [ServiceContract] interface IMyContract {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Allowed)]    void MyMethod(...); } [ServiceContract] interface IMyOtherContract {    [OperationContract]    [TransactionFlow(TransactionFlowOption.Mandatory)]    void MyOtherMethod(...); } class MyService : IMyContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyMethod(...)    {...} } class MyOtherService : IMyOtherContract {    [OperationBehavior(TransactionScopeRequired = true)]    public void MyOtherMethod(...)    {...} } ////////////////////////// Client Side //////////////////////////// using(TransactionScope scope = new TransactionScope( )) {    MyContractClient proxy1 = new MyContractClient( );    proxy1.MyMethod(...);    proxy1.Close( );    MyOtherContractClient proxy2 = new MyOtherContractClient( );    proxy2.MyOtherMethod(...);    proxy2.Close( );    scope.Complete( ); } //Can combine in single using block: using(MyContractClient proxy3 = new MyContractClient( )) using(MyOtherContractClient proxy4 = new MyOtherContractClient( )) using(TransactionScope scope = new TransactionScope( )) {    proxy3.MyMethod(...);    proxy4.MyOtherMethod(...);    scope.Complete( ); } 




Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net