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 committhe equivalent of the COM+
DisableCommit methodthat 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. |