Login | Register   
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Build a Framework for Managing Distributed Transactions in Three-tier Applications : Page 2

For many transactional applications in .NET, you can avoid the overhead of using Enterprise Services and simply mimic the simple transactional style of COM+ applications by using a DistributedTransaction object that wraps an ADO.NET Transaction object.


advertisement
The DistributedTransaction Object
This object must have an internal logic that mirrors that of a COM+ transaction, because it works through an ADO.NET transaction connected to a database. Similarly, it requires a state that specifies whether you can commit the transaction or should abort it. Here are the member variables:

Public Class DistributedTransaction Implements IDisposable Private _happy As Boolean = True Private _done As Boolean = False Private _sqlTransaction As SqlClient.SqlTransaction Private _connection As SqlConnection Private _disposed As Boolean = False End Class

In C#:

public class DistributedTransaction: IDisposable { private bool _happy = true; private bool _done = false; private SqlClient.SqlTransaction _sqlTransaction; private SqlConnection _ connection; private bool _disposed = false; }

You might think that in addition to _happy and _done you might also need to define corresponding Readonly properties, but, in fact, you don't need them. But you do need a Readonly property to access the internal ADO.NET transaction object, and through that send commands to the database. To protect the ADO.NET transaction from changing the value when the DistributedTransaction object's state doesn't permit this, you can check by passing a reference to the object to the caller. Here's the property's Get code:



Public ReadOnly Property SqlTransaction() As SqlClient.SqlTransaction Get ' -- check the object has not been disposed If _disposed Then Throw New ObjectDisposedException("Transaction") ' -- il object's lifetime has expired, caller cannot ' access the transaction If Me._done Then Throw New InvalidOperationException("Transaction has " & "been closed and can no longer be used") ' -- returns sqlTransaction internally referenced Return _sqlTransaction End Get End Property

In C#:

public SqlClient.SqlTransaction SqlTransaction { get { // check the object has not been disposed if (_disposed) { throw new ObjectDisposedException("Transaction"); } // if object's lifetime has expired, caller cannot // access the transaction if (this._done) throw new InvalidOperationException( "Transaction has been closed and can no longer " + "be used"); // returns sqlTransaction internally referenced return _sqlTransaction; } }

You can make the constructor start the transaction, because its lifecycle begins with object creation. If clients using the class manage it well, as per the pattern described later, there's no great penalty for opening the connection at the time of construction. This approach, furthermore, permits you to avoid consuming any business process resources if the database connection fails. However, you may want to change the code to open the connection just before any SQL commands execute rather than in the constructor. Note that if you take that tack, the client will detect connection errors only when it accesses the transaction for the first time (typically during execution of the first DAL method involved in the operation) rather than before the execution flow begins.

Public Sub New(ByVal connString As String) Try _connection = New SqlConnection(connString) ' — opens connection thereby ensuring ' connection validity Try _connection.Open() Catch e As SqlException ' -- throw exception with incorrect connstring Throw New ApplicationException( "Unable to open connection." & vbCrLf & connString) End Try ' -- starts transaction _sqlTransaction = _connection.BeginTransaction Catch ex As Exception ' -- try to close connection if it is open Try _connection.Close() Catch ex2 As Exception End Try Throw End Try End Sub

In C#:

public void DistributedTransaction (string connString) { // opens connection therefore checks if connString is valid try { _connection = new SqlConnection(connString); try { _connection.Open(); } catch (SqlException e) { // throw exception with incorrect connstring throw new ApplicationException("Unable to open " + "connection.\r\n" + connString); } // starts transaction _sqlTransaction = _connection.BeginTransaction(); } catch (Exception ex) { // try to close connection if it is open try { _connection.Close(); } catch (Exception ex2) {} throw; } }

To avoid having to pass the connection string from the business layer (the layer in which the DistributedTransaction class normally gets instantiated), you can define a shared property or method in the DAL that returns an instantiated DistributedTransaction object, limiting the scope for the connection string to the DAL only.

Now you need some methods to change the transaction's status. First, here's the code to disable commit—the equivalent of the COM+ DisableCommit method—that the DAL methods may call if runtime errors occur.

Public Sub DisableCommit() ' -- checks that the object has not been disposed If _disposed Then Throw New ObjectDisposedException("SqlTransaction") Me._happy = False End Sub

In C#:

public void DisableCommit{ // checks the object has not been disposed if (disposed) { throw new ObjectDisposedException("SqlTransaction"); this._happy = false; } }

You can also define a corresponding EnableCommit method, as in COM+, but as you'll seldom use that in practice (in COM+, DisableCommit and EnableCommit have a role that goes far beyond the scope of this framework). But it's no big problem to implement, and it can sometimes be useful.

The second method you need is one that commits the transaction. Clients can call it only when the DistributedTransaction object is in a valid state. After the transaction has been committed, you should immediately close the database connection to free resources, and update the _done property to show that its lifecycle has ended.

Public Sub Commit() ' -- checks the object has not been disposed If _disposed Then Throw New ObjectDisposedException("SqlTransaction") ' -- checks a commit or rollback has not been executed If _done Then Throw New InvalidOperationException( "Transaction has yet been committed/rolled back") ' -- checks we are happy and therefore can commit ' the transaction If Not _happy Then Throw New InvalidOperationException( "Transaction has commit disabled and cannot " & "be commited") Try ' -- commits the transaction _sqlTransaction.Commit() Catch ex As Exception Throw Finally ' -- updates status to show this object's lifetime ' has expired _done = True ' -- Always close underlying database connection If Not _sqlTransaction.Connection Is Nothing Then If _sqlTransaction.Connection.State = ConnectionState.Open Then _sqlTransaction.Connection.Close() End If _sqlTransaction.Connection.Dispose() End If ' -- Disposed transaction is no longer useful _sqlTransaction.Dispose() End Try End Sub

In C#:

public void Commit() { // checks the object has not been disposed if (_disposed) { throw new ObjectDisposedException("SqlTransaction"); } // checks a commit or rollback has not been execute if (_done) { throw new InvalidOperationException( "Transaction has yet been committed/rolled back"); } // checks we are happy and can therefore commit // the transaction if (!_happy) { throw new InvalidOperationException( "Transaction has commit disabled and cannot " + "be commited"); } try { // commits the transaction _sqlTransaction.Commit(); } catch (Exception ex) { throw; } finally { // updates status to show this object's lifetime // has expired _done = true; // Always close underlying database connection if (_sqlTransaction.Connection != null) { if (_sqlTransaction.Connection.State == ConnectionState.Open){ _sqlTransaction.Connection.Close(); } _sqlTransaction.Connection.Dispose(); } // Disposed transaction that now is no more useful sqlTransaction.Dispose(); } }

Just as you must have a Commit method, you must have an Abort. In that case you also need to check that the status is OK before continuing; after executing the operation, you can close database connections and update the internal object's status

Public Sub Abort() ' -- checks if the object has been disposed If _disposed Then Throw New _ ObjectDisposedException("SqlTransaction") ' -- checks a commit or rollback has not been executed If _done Then Throw New InvalidOperationException( "Transaction has yet been committed/rolled back") Try ' -- Rolls back the transaction _sqlTransaction.Rollback() ' -- updates status to unhappy _happy = False Catch ex As Exception Throw Finally ' -- update status because object's lifetime has expired _done = True ' -- closes underlying connection to database If Not _sqlTransaction.Connection Is Nothing Then If _sqlTransaction.Connection.State = ConnectionState.Open Then _sqlTransaction.Connection.Close() End If _sqlTransaction.Connection.Dispose() End If ' -- Disposes transaction that now is no more useful _sqlTransaction.Dispose() End Try End Sub

In C#:

public void Abort(){ // checks if the object has been disposed if (_disposed) { throw new ObjectDisposedException("SqlTransaction"); } // checks a commit or rollback has not been executed if (_done) { throw new InvalidOperationException( "Transaction has yet been committed/rolled back"); } try { // Rollbacks the transaction _sqlTransaction.Rollback(); // updates status to unhappy _happy = false; } catch (Exception ex) { throw; } finally { // updates status because object's lifetime has expired _done = true; // closes underling connection to database if (!_sqlTransaction.Connection == null) { if (_sqlTransaction.Connection.State == ConnectionState.Open) { _sqlTransaction.Connection.Close(); } _sqlTransaction.Connection.Dispose(); } // Disposes transaction that is no longer useful _sqlTransaction.Dispose(); } }

Author's Note: In the preceding code you may have noticed references to _disposed, which is a private variable updated when the Dispose method is called in the standard IDisposable implementation pattern, which I'll discuss later.



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap