System.Transactions


The namespace System.Transaction has been available since .NET 2.0 and brings a new transaction programming model to .NET applications. Figure 21-3 shows a Visual Studio class diagram with the transaction classes, and their relationships, from the System.Transaction namespace: Transaction, CommittableTransaction, DependentTransaction, and SubordinateTransaction.

image from book
Figure 21-3

Transaction is the base class of all transaction classes and defines properties, methods, and events available with all transaction classes. CommittableTransaction is the only transaction class that supports committing. This class has a Commit() method; all the other transaction classes just can do a rollback. The class DependentTransaction is used with transactions that are dependent on other transaction. A dependent transaction can depend on a transaction created from the committable transaction. Then the dependent transaction adds to the outcome of the committable transaction if it is successful or not. The class SubordinateTransaction is used in conjunction with the Distributed Transaction Coordinator (DTC). This class represents a transaction that is not a root transaction but can be managed by the DTC.

Open table as spreadsheet

Transaction Class Members

Description

Current

The property Current is a static property without the need to have an instance. Transaction.Current returns an ambient transaction if one exists. Ambient transactions are discussed later.

IsolationLevel

The IsolationLevel property returns an object of type IsolationLevel. IsolationLevel is an enumeration that defines what access other transactions have to the interim results of the transaction. This affects the I of ACID; not all transactions are isolated.

TransactionInformation

The TransactionInformation property returns a TransactionInformation object. TransactionInformation gives you information about the current state of the transaction, the time when the transaction was created and transaction identifiers.

EnlistVolatile()

EnlistDurable()

EnlistPromotableSinglePhase()

With the enlist methods EnlistVolatile(), EnlistDurable(), and EnlistPromotableSinglePhase() you can enlist custom resource managers that participate with the transaction.

Rollback()

With the Rollback() method, you can abort a transaction and undo everything to set all results to the state before the transaction.

DependentClone()

With the DependentClone() method, you can create a transaction that depends on the current transaction.

TransactionCompleted

TransactionCompleted is an event that is fired when the transaction is completed - either successful or not successful. With the event handler an object of type

For demonstrating the features of System.Transaction, the class Utilities inside a separate assembly offers some static methods. The method AbortTx() returns true or false depending on the input from the user. The method DisplayTransactionInformation() gets a TransactionInformation object as parameter and displays all the information from the transaction: creation time, status, local, and distributed identifiers:

  public static class Utilities {    public static bool AbortTx()    {       Console.Write("Abort the Transaction (y/n)?");       return Console.ReadLine() == "y";    }    public static void DisplayTransactionInformation(          TransactionInformation ti)    {       if (ti != null)       {          Console.WriteLine("Creation Time: {0:T}", ti.CreationTime);          Console.WriteLine("Status: {0}", ti.Status);          Console.WriteLine("Local ID: {0}", ti.LocalIdentifier);          Console.WriteLine("Distributed ID: {0}", ti.DistributedIdentifier);          Console.WriteLine();       }    } } 

Committable Transactions

The Transaction class cannot be committed programmatically; it doesn’t have a method to commit the transaction. The base class Transaction just supports aborting the transaction. The only transaction class that supports a commit is the class CommittableTransaction.

With ADO.NET a transaction can be enlisted with the connection. To make this possible, an AddStudent() method is added to the class StudentData that accepts a System.Transactions.Transaction object as second parameter. The object tx is enlisted with the connection by calling the method EnlistTransaction of the SqlConnection class. This way the ADO.NET connection is associated with the transaction.

  public void AddStudent(Student s, Transaction tx) {     SqlConnection connection = new SqlConnection(           Properties.Settings.Default.CourseManagementConnectionString);     connection.Open();     try     {        if (tx != null)           connection.EnlistTransaction(tx);        SqlCommand command = connection.CreateCommand();        command.CommandText = "INSERT INTO Students (Firstname, Lastname, Company)              VALUES (@Firstname, @Lastname, @Company)";        command.Parameters.AddWithValue("@Firstname", s.Firstname);        command.Parameters.AddWithValue("@Lastname", s.Lastname);        command.Parameters.AddWithValue("@Company", s.Company);        command.ExecuteNonQuery();     }     finally     {        connection.Close();     } } 

In the Main() method of the console application CommittableTransaction, first a transaction of type CommittableTransaction is created. After creation of the transaction, information is shown on the console. Then a Student object is created, and this object is written to the database from the AddStudent() method. If you verify the record in the database from outside of the transaction, you cannot see the student added until the transaction is completed. In case the transaction fails, there’s a rollback and the student is not written to the database.

After the AddStudent() method is invoked, the helper method Utilities.AbortTx() is called to ask if the transaction should be aborted. In cases the user aborts, an exception of type ApplicationException is thrown and in the catch block a rollback with the transaction is done by calling the method Rollback() of the Transaction class. The record is not written to the database. If the user does not abort, the Commit() method commits the transaction, and the final state of the transaction is committed.

  static void Main() {    CommittableTransaction tx = new CommittableTransaction();    Utilities.DisplayTransactionInformation("TX created",          tx.TransactionInformation);    try    {       Student s1 = new Student();       s1.Firstname = "Neno";       s1.Lastname = "Loye";       s1.Company = "thinktecture";       StudentData db = new StudentData();       db.AddStudent(s1, tx);       if (Utilities.AbortTx())       {          throw new ApplicationException("transaction abort");       }        tx.Commit();    }    catch (Exception ex)    {       Console.WriteLine(ex.Message);       Console.WriteLine();       tx.Rollback();    }    Utilities.DisplayTransactionInformation(tx.TransactionInformation); } 

Here, you can see the output of the application where the transaction is active and has a local identifier. With the first run abort is selected. After the transaction is finished, you can see the aborted state.

 TX Created Creation Time: 7:30:49 PM Status: Active Local ID: :1 Distributed ID:  Abort the Transaction (y/n)? y Transaction abort TX finished Creation Time: 7:30:49 PM Status: Aborted Local ID: :1 Distributed ID:  Press any key to continue ...

The second run of the application does not abort the transaction. The transaction has the status committed, and the data is written to the database.

 TX Created Creation Time: 7:33:04 PM Status: Active Local ID: :1 Distributed ID:  Abort the Transaction (y/n)? n TX finished Creation Time: 7:33:04 PM Status: Committed Local ID: :1 Distributed ID:  Press any key to continue ...

Transaction Promotion

System.Transactions supports promotable transactions. Depending on the resources that participate with the transaction, either a local or a distributed transaction is created. SQL Server 2005 supports promotable transactions, and so far you’ve seen only local transactions. For now, the distributed ID was always set to 0. With a resource that does not support promotable transactions, a distributed transaction is created. If multiple resources are added to the transaction, the transaction may start with a local transaction and promote to a distributed transaction as required. Such a promotion happens when multiple SQL Server 2005 database connections are added to the transaction. The transaction starts as a local transaction and then is promoted to a distributed transaction.

The console application is now changed in that a second student is added by using the same transaction object tx. As every AddStudent() method opens a new connection, two connections are associated with the transaction after the second student is added.

 static void Main() {    CommittableTransaction tx = new CommittableTransaction();    Utilities.DisplayTransactionInformation("TX created",          tx.TransactionInformation);    try    {       Student s1 = new Student();       s1.Firstname = "Neno";       s1.Lastname = "Loye";       s1.Company = "thinktecture";       StudentData db = new StudentData();       db.AddStudent(s1, tx);       Student s2 = new Student();       s2.Firstname = "Dominick";       s2.Lastname = "Baier";       s2.Company = "thinktecture";       db.AddStudent(s2, tx);        Utilities.DisplayTransactionInformation("2nd connection enlisted",             tx.TransactionInformation);       if (Utilities.AbortTx())       {          throw new ApplicationException("transaction abort");       }       tx.Commit();    }    catch (Exception ex)    {       Console.WriteLine(ex.Message);       Console.WriteLine();       tx.Rollback();    }    Utilities.DisplayTransactionInformation("TX finished",          tx.TransactionInformation); }

Running the application now, you can see with the first student added that the distributed identifier is 0, but with the second student added the transaction was promoted so a distributed identifier is associated with the transaction.

 TX created Creation Time: 7:56:24 PM Status: Active Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 2nd connection enlisted Creation Time: 7:56:24 PM Status: Active Local ID: :1 Distributed ID: d Abort the Transaction (y/n)?

Transaction promotion requires the Distributed Transaction Coordinator (DTC) to be started. If promoting transactions fails with your system, verify that the DTC service is started. Starting the Component Services MMC snap-in, you can see the actual status of all DTC transactions running on your system. Selecting Transaction List on the tree view, you can see all active transactions. In Figure 21-4, you can see that there’s a transaction active with the same distributed identifier as was shown with the console output earlier. If you verify the output on your system, make sure that the transaction has a timeout and aborts in case the timeout is reached. After the timeout, you can’t see the transaction in the transaction list anymore. You can also verify the transaction statistics with the same tool. Transaction Statistics shows the number of committed and aborted transactions.

image from book
Figure 21-4

Dependent Transactions

With dependent transactions, you can influence one transaction from multiple threads. A dependent transaction depends on another transaction and influences the outcome of the transaction.

The sample application DependentTransactions creates a dependent transaction for a new thread. The method TxThread() is the method of the new thread where a DependentTransaction object is passed as a parameter. Information about the dependent transaction is shown with the helper method DisplayTransactionInformation(). Before the thread exits, the Complete() method of the dependent transaction is invoked for to define the outcome of the transaction. A dependent transaction can define the outcome of the transaction by calling either the Complete() or Rollback() method. The Complete() method sets the success bit. If the root transaction finishes, and if all dependent transactions have set the success bit set to true, the transaction commits. If any of the dependent transactions set the abort bit aborts by invoking the Rollback() method, the complete transaction aborts.

  static void TxThread(object obj) {    DependentTransaction tx = obj as DependentTransaction;    Utilities.DisplayTransactionInformation("Dependent Transaction",          tx.TransactionInformation);    Thread.Sleep(3000);    tx. Complete();    Utilities.DisplayTransactionInformation("Dependent TX Complete",          tx.TransactionInformation); } 

With the Main() method, first a root transaction is created by instantiating the class CommittableTransaction, and the transaction information is shown. Next, the method tx.DependentClone() creates a dependent transaction. This dependent transaction is passed to the method TxThread() that is defined as the entry point of a new thread.

The method DependentClone() requires an argument of type DependentCloneOption. DependentCloneOption is an enumeration with the values BlockCommitUntilComplete and RollbackIfNotComplete. This option is important if the root transaction completes before the dependent transaction. Setting the option to RollbackIfNotComplete, the transaction aborts if the dependent transaction didn’t invoke the Complete() method before the Commit() method of the root transaction. Setting the option to BlockCommitUntilComplete, the method Commit() waits until the outcome is defined by all dependent transactions.

Next, the Commit() method of the CommittableTransaction class is invoked if the user does not abort the transaction.

Tip 

Chapter 18, “Threading and Synchronization,” covers these subjects.

  static void Main() {    CommittableTransaction tx = new CommittableTransaction();    Utilities.DisplayTransactionInformation("Root TX created",          tx.TransactionInformation);    try    {       new Thread(TxThread).Start(             tx.DependentClone(DependentCloneOption.BlockCommitUntilComplete));       if (Utilities.AbortTx())       {          throw new ApplicationException("transaction abort");       }       tx.Commit();    }    catch (Exception ex)    {       Console.WriteLine(ex.Message);       tx.Rollback();    }    Utilities.DisplayTransactionInformation("TX finished",          tx.TransactionInformation); } 

With the output of the application, you can see the root transaction with its identifier. Because of the option DependentCloneOption.BlockCommitUntilComplete, the root transaction waits in the Commit() method until the outcome of the dependent transaction is defined. As soon as the dependent transaction is finished, the transaction is committed.

 Root TX created Creation Time: 8:35:25 PM Status: Active Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Abort the Transaction (y/n)? n Dependent Transaction Creation Time: 8:35:25 PM Status: Active Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Dependent TX Complete Root TX finished Creation Time: 8:35:25 PM Status: Committed Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Creation Time: 8:35:25 PM Status: Committed Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Press any key to continue ...

Ambient Transactions

The really big advantage of System.Transactions is the ambient transactions feature. With ambient transactions, there’s no need to manually enlist a connection with a transaction; this is done automatically from the resources supporting ambient transactions.

An ambient transaction is associated with the current thread. You can get and set the ambient transaction with the static property Transaction.Current. APIs supporting ambient transactions check this property to get an ambient transaction, and enlist with the transaction. ADO.NET connections support ambient transactions.

You can create a CommittableTransaction object and assign it to the property Transaction.Current to initialize the ambient transaction. Another way to create ambient transactions is with the TransactionScope class. The constructor of the TransactionScope creates an ambient transaction. Because of the implemented interface IDisposable, you can use a transaction scope easily with the using statement.

The members of TransactionScope are listed in the following table.

Open table as spreadsheet

TransactionScope Members

Description

Constructor

With the constructor of TransactionScope you can define the transactional requirements. You can also pass an existing transaction and define the transaction timeout.

Complete()

Invoking the Complete() method, you set the success bit of the transaction scope.

Dispose()

The Dispose() method completes the scope and commits or aborts the transaction if the scope is associated with the root transaction. If the success bit is set with all dependent transactions, the Dipose() method commits; otherwise, a rollback is done.

As the TransactionScope class implements the IDisposable interface, you can define the scope with the using statement. The default constructor creates a new transaction. Immediately after creating the TransactionScope instance, the transaction is accessed with the get accessor of the property Transaction .Current to display the transaction information on the console.

To get the information when the transaction is completed, the method OnTransactionCompleted() is set to the TransactionCompleted event of the ambient transaction.

Then a new Student object is created and written to the database by calling the StudentData .AddStudent() method. With ambient transactions, it is no longer necessary to pass a Transaction object to this method, because the SqlConnection class supports ambient transactions and automatically enlists it with the connection. Then the Complete() method of the TransactionScope class sets the success bit, and with the end of the using statement the TransactionScope is disposed, and thus a commit is done. If the Complete() method is not invoked, the Dispose() method aborts the transaction.

Tip 

If an ADO.NET connection should not enlist with an ambient transaction, you can set the value Enlist=false with the connection string.

  static void Main() {    using (TransactionScope scope = new TransactionScope())    {       Transaction.Current.TransactionCompleted += OnTransactionCompleted;       Utilities.DisplayTransactionInformation("Ambient TX created",             Transaction.Current.TransactionInformation);       Student s1 = new Student();       s1.Firstname = "Ingo";       s1.Lastname = "Rammer";       s1.Company = "thinktecture";       StudentData db = new StudentData();       db.AddStudent(s1);       if (!Utilities.AbortTx())          scope.Complete();       else          Console.WriteLine("transaction will be aborted");    } } // Dispose static void OnTransactionCompleted(object sender, TransactionEventArgs e) {    TransactionInformation ti = e.Transaction.TransactionInformation;    Console.WriteLine("Transaction completed; status: {0}, id: {1}",          ti.Status, ti.LocalIdentifier); } 

Running the application, you can see an active ambient transaction after an instance of the TransactionScope class is created. The last output of the application is the output from the TransactionCompleted event handler to display the finish transaction state.

Ambient TX created Creation Time: 9:55:40 PM Status: Active Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Abort the Transaction (y/n)? n Transaction completed; status: Committed, id: :1 Press any key to continue ...

Nested Scopes with Ambient Transactions

With the TransactionScope class you can also nest scopes. The nested scope can be directly inside the scope or within a method that is invoked from a scope. A nested scope can use the same transaction as the outer scope, suppress the transaction, or create a new transaction that is independent from the outer scope. The requirement for the scope is defined with a TransactionScopeOption enumeration that is passed to the constructor of the TransactionScope class.

The values available with the TransactionScopeOption enumeration and their functionality are described in the following table.

Open table as spreadsheet

TransactionScopeOption Member

Description

Required

Required defines that the scope requires a transaction. If the outer scope already contains an ambient transaction, the inner scope uses the existing transaction. If an ambient transaction does not exist, a new transaction is created.

If both scopes share the same transaction, every scope influences the outcome of the transaction. Only if all scopes set the success bit can the transaction commit. If one scope does not invoke the Complete() method before the root scope is disposed of, the transaction is aborted.

RequiresNew

RequiresNew always creates a new transaction. If the outer scope already defines a transaction, the transaction from the inner scope is completely independent. Both transactions can commit or abort independently.

Suppress

With Suppress the scope does not contain an ambient transaction no matter if the outer scope contains a transaction or not.

The next sample defines two scopes, where the inner scope is configured to require a new transaction with the option TransactionScopeOption.RequiresNew.

 using (TransactionScope scope = new TransactionScope()) {    Transaction.Current.TransactionCompleted += OnTransactionCompleted;    Utilities.DisplayTransactionInformation("Ambient TX created",          Transaction.Current.TransactionInformation);              using (TransactionScope scope2 =          new TransactionScope(TransactionScopeOption.RequiresNew))    {       Transaction.Current.TransactionCompleted += OnTransactionCompleted;        Utilities.DisplayTransactionInformation(              "Inner Transaction Scope",              Transaction.Current.TransactionInformation);        scope2.Complete();    }    scope.Complete(); }

Running the application, you can see that both scopes have different transaction identifiers, although the same thread is used. Having one thread with different ambient transactions because of different scopes, the transaction identifier differs in the last number following the GUID.

 Ambient TX created Creation Time: 11:01:09 PM Status: Active Local ID: :1 Distributed ID: 00000000-0000-0000-0000-0000000000 Inner Transaction Scope Creation Time: 11:01:09 PM Status: Active Local ID: :2 Distributed ID: 00000000-0000-0000-0000-0000000000 Transaction completed: status: Committed, id: :2 Transaction completed: status: Committed, id: :1

Important 

If multiple threads should use the same ambient transaction, you cannot just create a transactional scope and create a new thread within this scope. Because ambient transactions are bound to a thread, the newly created thread doesn’t have an ambient transaction. If this is required, you can create a dependent transaction that depends on the ambient transaction. Within the new thread, you can assign the dependent transaction to the ambient transaction.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

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