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.
The most common way of using transactions explicitly is via the TransactionScope
class:
public class TransactionScope : IDisposable
{
public TransactionScope( );
//Additional constructorspublic 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 object—if 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.
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; }
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.
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.
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 thenscope.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 thenscope.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 thenscope.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( )
).
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 thenscope.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.
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.
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.
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.
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.
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( ); }