devxlogo

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

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

ne of the best features of developing COM+ based applications in standard three-tier presentation, business, and data access (DAL) layers is the simplicity of managing transactions distributed between the application’s layers. I’m not referring to the canonical definition of “distributed transactions”?that is “distributed between different databases,” but to “distributed between a single application’s different logical layers and methods.”

Developing a transactional business process (such as inserting a new order, updating a user’s data, activating a new account, etc.) in the COM+ environment is disarmingly linear. It requires:

  • A method in the business layer that implicitly begins a transaction.
  • Calling all the DAL or business layer’s methods required to define the operation’s flow.
  • Committing or rolling back the transaction initiated by the starting method.

The intermediate methods involved in the transaction (in general all DAL methods) are limited to calling DisableCommit on the execution’s context, while only the starting method (the transaction’s manager) may call a commit, if all methods involved in the transaction succeed or an abort if logical or runtime errors occur.

Unfortunately, you’ll probably recall this clear and simple-to-implement three-part procedure with nostalgia when you switch to .NET. Yes, you can use the .NET Enterprise Services for transactions, but I think that using Enterprise Services adds complications that are worse than the benefits, so I prefer to use only ADO.NET resources.

ADO.NET does provide a good resource with its Transaction object, but by itself that doesn’t permit you to implement cross-layer transactions in a simple fashion. That’s not because it lacks functionality (if you try, you can approach the functionality of the steps described above), but because to achieve the goal you must create (and duplicate) similar code in every method involved in the transaction?in fact, in every business process layer and DAL method), which is a boring, expensive, and ultimately unaffordable solution.

To circumvent such problems this article describes a small transactional-support framework that you can use to write .NET code using the same three-step process described above that you formerly used in the COM+ environment?but without using Enterprise Services. The resulting applications require minimal code to manage transactions distributed between layers, and let you reuse methods to define different operations and flows that would otherwise prove expensive to maintain.

Framework Structure
This framework is based on the idea that it’s possible to create a “DistributedTransaction” .NET object that you can use in the same manner as in classic COM+. This DistributedTransaction object is a wrapper built around the ADO.NET Transaction object, which exposes the fundamental functionality. But the new object results in simpler access to data, and makes distributed transactions available across all modules in a three-tier application.

The only formal difference when implementing solutions based on this framework as compared with COM+, is that now the business layer’s method must always explicitly instantiate the transaction, and you must pass the DistributedTransaction object through calls that clearly are not executed inside a uniform context managed by an external environment, as in COM+. Otherwise, it’s just as simple as using COM+; DAL methods work autonomously, and they can optionally call DisableCommit when a problem occurs. The business layer’s methods may call Commit if all the operations succeed, or SetAbort, if logical or runtime errors occurred during flow’s execution.

A Typical COM+ Application
Before analyzing the framework, here’s a simple typical COM+ application, written in VB6, that defines a distributed transaction.

The DAL’s method has its MTSTransactionMode property set to UsesTransaction or RequiresTransaction, depending on its logical characteristics, and it doesn’t contain explicit code to manage transactions. To add error handling in the method, you can call GetObjectContext.DisableCommit; but if you’re satisfied with having runtime errors handled by the caller, and not interested in registering the method’s internal status (this is generally acceptable), you can simply ignore error-handling, and ignore any distributed transaction problems altogether.

The following business layer method initiates and completes the transaction. Its MTSTransactionMode property would be set to either RequiresNewTransaction or RequiresTransaction. The implementation inserts two code elements to manage the distributed transaction: a call to GetObjectContext.SetComplete that executes to commit the transaction, and a general error handler, that calls GetObjectContext.SetAbort to rollback the transaction if errors occur.

   Public Function DoSomething(      ByVal inputData As Object) As Integer         On Error GoTo ErrorHandler         '' [...] Executes business operations         '' - Checks for business rules      If rulesValidationSuccess = False Then         '' -- sets an error code to return         DoSomething = -20         ' -- rollback transaction         GetObjectContext.SetAbort         Exit Function      End If         ' - Commits transaction      GetObjectContext.SetComplete         ' [...] Releases objects and/or peforms non-transactional       ' operations (eg. sends an emails)         ' -- manages global errors   ErrorHandler:      ' -- rolls back transaction      GetObjectContext.SetAbort      '[...] Releases resources, logs error, etc.      End Function

With just these few lines of code you can define all the business flows you need, using similar business layer or DAL methods. This structure is a lot simpler than the equivalent version made with standard .NET and ADO.NET base objects.

Framework Elements
The framework consists of an object that implements its logic and a pattern you can use to write business and data layer methods that exploit its benefits. But before defining that object, remember that a COM+ transaction’s state is defined by two bits, called happy and done. The happy state represents a transaction that has not yet been committed?even if a method involved in the transaction has called DisableCommit, but no rollback has yet been executed. The done state represents a transaction that has been closed (either committed or rolled back), and is therefore no longer usable. Indeed, committing or rolling back a transaction releases all resources on the underlying database, and ends its lifecycle. In other words, if you still need to call other commands, you need to create and work with a new transaction context. With that in mind, here’s how to create the DistributedTransaction wrapper object.

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.
" + 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.

Using DistributedTransaction in the DAL and Business Layers
Now that you have a distributed transaction object, you can use it while implementing client code.

The DAL’s methods should always receive a DistributedTransaction object as an input parameter, already instantiated and ready to work. In general you can avoid inserting other explicit code to manage the distributed transaction (as in COM+); for example, a DAL method might look like this:

   Public Sub SaveSomething(byVal Input as Object,       ByVal transaction As DistributedTransaction)         ' -- [...] Saves data using transaction.SqlTransaction       ' to send SQL commands      End sub

In C#:

   public void SaveSomething(object Input,       DistributedTransaction transaction) {      // [...] Saves data using transaction.SqlTransaction       // to send SQL commands   }

To handle internal errors, use the DisableCommit method defined earlier, in the same way you would have previously used it in COM+. For example:

   Public Sub SaveSomething(byVal Input as Object,       ByVal transaction As DistribuitedTransaction)      Try         ' -- check parameters         If transaction Is Nothing Then Throw New             ArgumentNullException("transaction cannot be null")            If input Is Nothing Then Exit Sub            ' -- [...] Prepares parameters and executes command          ' on transaction.SqlTransaction         Catch ex As Exception         ' -- disables transaction         transaction.DisableCommit()             ' -- [...] Logs detailed error         Throw         End Try   End Sub

In C#:

   public void SaveSomething(object Input,       DistributedTransaction transaction){       try {         // check parameters         if (transaction == null) {            throw new ArgumentNullException(            "transaction cannot be null");         }            // -- [...] Prepares parameters and executes command on          // transaction.SqlTransaction         }      catch (Exception ex) {         // disables transaction         transaction.DisableCommit();            // [...] Logs detailed error            throw;       }   }

When an error occurs, the method disables the transaction (without directly calling SetAbort, so other methods that use the transaction can continue to work, reproducing the typical COM+ pattern) and propagates one exception that the client may handle. When the method needs to interact with the database, it can use the ADO.NET transaction object exposed by the DistributedTransaction.SqlTransaction property, without creating new connections.

Inside the Business Layer
To start a transaction, business layer methods should instantiate a DistributedTransaction object, pass it to DAL methods, handle exceptions, and finally, commit or roll back the transaction.

The important code of such methods is the part that handles the operation’s result, committing or rolling back the transaction. It also needs to ensure that resources used by DistributedTransaction object get released. Typical code would follow the pattern shown below.

   Public Sub DoSomething (ByVal input as Object)         ' [...] Validates parameters         ' -- Defines transactional context for business operations      Dim tr As DistribuitedTransaction         Try         ' -- instances DAL         Dim myData As New DAL         ' -- starts transaction         tr = New Transaction(_connstring)            ' [...] Calls DAL method            ' -- commits transaction         tr.Commit()          Catch ex As Exception          ' -- Rollback Transaction          tr.Abort()             ' [...] Logs error          Throw ' some user defined error...         Finally         ' -- Releases transaction         tr.Dispose()         End Try      End Sub

In C#:

   public void DoSomething(object input) {         // [...] Validates parameters         // Defines transactional context for business operations      DistribuitedTransaction tr;         try {         // instances DAL         DAL myData = new DAL();            // starts transaction         tr = new Transaction(_connstring);            // [...] Calls DAL method            // commits transaction         tr.Commit();         }       catch (Exception ex) {             // Rolls back Transaction         tr.Abort();            throw;      }       finally {         // Releases transaction         tr.Dispose();      }   }

Making Business-to-business Calls
Sometimes you need to implement distributed transactions involving different business layer methods in addition to DAL methods. If all the operations execute inside different transactions (where the business layer code is set to the RequiresNewTransaction COM+ pattern), everything works according to the sequence already shown. However, if you need to execute multiple operations inside the same transaction’s context (the RequiresTransaction pattern), you must provide two overloads for the business layer methods involved. The first accepts a DistributedTransaction object as an input parameter; the second does not. Otherwise, all the other parameters are identical. In this scenario, the overload that requires the DistributedTransaction parameter is a “secondary” method that implements a local business operation, acting as a server for the “primary” methods that start transactions and define business operations at a higher level.

The overload that doesn’t take a DistributedTransaction looks like this:

   ' -- First overload without transaction   Public Sub DoSomething (ByVal input as Object)         ' [...] Validates parameters         ' -- Defines transactional context for business operations      Dim tr As DistribuitedTransaction         Try         ' -- starts transaction         tr = New Transaction(_connstring)            ' - Calls business method that requires transaction         DoSomething (input, tr)            ' [...] If required, can also call other business or          ' data methods passing tr as context            ' -- commits transaction         tr.Commit()         Catch ex As Exception         ' -- Rolls back Transaction         tr.Abort()             ' [...] Logs error         Throw ' some user defined error...         Finally         ' -- Releases transaction         tr.Dispose()         End Try   End Sub   

Here’s the overload that accepts a DistributedTransaction.

   ' -- Second overload with transaction   Friend Sub DoSomething (ByVal input as Object,       ByVal tr As Transaction)         '— [...] checks parameter         Try         Dim myData As New DAL            myData.DoSomething (input, tr)         Catch ex As Exception         ' -- disable transaction's commit         tr.DisableCommit()         ' [...] Logs error         Throw         End Try   End Sub

In C#:

   // First overload without transaction   public void DoSomething(object input) {         // [...] Validates parameters         // Defines transactional context for business operations      DistribuitedTransaction tr;         try {            // starts transaction         tr = new Transaction(_connstring);            // Calls business method that requires transaction         DoSomething(input, tr);            // [...] If required, can also call other business or          // data methods passing tr as context            // commits transaction         tr.Commit();         }       catch (Exception ex) {         // Rolls back Transaction         tr.Abort();            // [...] Logs error            throw;      }       finally {         // Releases transaction         tr.Dispose();      }   }      // Second overload with transaction   internal void DoSomething(object input, Transaction tr) {         // [...] checks parameter         try {            DAL myData = new DAL();         myData.DoSomething(input, tr);         }       catch (Exception ex) {            // disable transaction's commit         tr.DisableCommit();            throw;      }   }

The first method creates the transaction and calls the overload that requires it as input. This method works exactly as described earlier for simpler scenarios, except that it uses the passed-in DistributedTransaction object instead of creating a new one. Note its error handler, which acts like error handlers typically found in DAL methods. In other words, it calls DisableCommit rather than rolling back the transaction, because this particular business layer method has a secondary role, just as DAL methods usually do. It’s usually better to define such support methods as Private or Friend, so they can’t be exposed outside the project; however, sometimes the assembly structure will require you to distribute the business operation in such a way that you must define all overloads as Public. In this particular case, the Fa?ade layer’s components expose and map only business methods that don’t require transactions.

With this last structure in place, you have in fact replicated the NewTransaction and RequiresNewTransaction COM+ patterns used in a declarative way in VB6 and other COM-based languages. You obviously still need code to implement the various layers, but now you can write that code in a clear linear fashion that’s much simpler than using only the ADO.NET base objects.

Implementing Dispose
I noted earlier that the DistributedTransaction object implements the IDisposable pattern. In transactional applications, it’s of paramount importance that you always either commit or rollback a transaction, so implementing IDisposable is required to close the transaction even if a developer forgets to explicitly insert the required code in a business layer method. While it’s preferable for client code to explicitly commit or rollback the transaction, you can’t guarantee that will occur, so it’s best to write a robust framework that guarantees fallback transaction management. The DistributedTransaction object can’t call the Dispose method, so the client developer should always call it. The code for the Dispose method looks like this:

   Protected Sub Dispose(ByVal disposing As Boolean)         If _disposed Then Exit Sub         If disposing Then            ' -- Avoid runtime error when disposing          ' (connction broken, etc.)         Try            ' -- if transaction has not been committed or rolled             ' back we need to close it            If Not _done Then               ' -- Commits if the object is happy,                ' otherwise rolls back               If Me._happy Then                  _sqlTransaction.Commit()               Else                  _sqlTransaction.Rollback()               End If            End If         Catch ex As Exception            End Try            Try            ' -- Important: closes and releases reference             ' to transaction            _connection.Close()            _connection.Dispose()            _sqlTransaction.Dispose()         Catch ex As Exception            Debug.WriteLine(               "Error closing transaction's resources: "                & ex.Message)         End Try         End If         _disposed = True      End Sub

In C#:

   protected void Dispose(bool disposing){         if (_disposed) {return;}         if (disposing) {            // Avoid runtime error when disposing          // (connection broken, etc.)         try {               // if transaction has not been committed or             // rolled back, we need to close it               // Commits if the object is happy, otherwise             // rolls back            if (!_done) {               if (this._happy) {                  _sqlTransaction.Commit();               }                else {                  _sqlTransaction.Rollback();               }            }            }          catch (Exception ex) {}            try{            // -- Important: closes and releases reference to             // transaction            _connection.Close();            connection.Dispose();            _sqlTransaction.Dispose();         }          catch (Exception ex) {            Debug.WriteLine("Error closing transaction's " +                resources: " + ex.Message);         }         }          _disposed = true;    }

Clearly, you may implement the standard IDisposable pattern, so you may define these other standard functions:

Public Sub Dispose() Implements System.IDisposable.Dispose   ' -- makes resources cleanup   Dispose(True)   ' -- suppress call to finalize   GC.SuppressFinalize(Me)End SubProtected Overrides Sub Finalize()   ' -- release all unmanaged resources   Dispose(False)End Sub

In C#:

public void System.IDisposable.Dispose() {   // makes resources cleanup   Dispose(true);   // suppress call to finalize   GC.SuppressFinalize(this);}protected void Finalize {   ' -- release all unmanaged resources   Dispose(false);}

Creating a distributed transaction schema applied to the application layer requires considerably more code and work in .NET than in COM+. Unfortunately, using Enterprise Services carries baggage that you may want to avoid, unless you need it for more than just transactions. The framework described in this article, built around the ADO.NET Transaction object, lets you write business and data access layer methods in a manner similar to COM+, using little extra code and with a similarly efficient structure. Of course, COM+ and its transactions can provide more functionality than is captured in this framework, but for basic transactions, this framework should provide all you need.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist