Introduction
Here, I will be discussing 'How to implement a Transaction in .NET application' but it is not to be confused with 'System.Transactions'
namespace available in .NET Framework and 'Transaction in SQL Server'.
I will explain 'how to implement transaction in your class library'.
I will not touch transaction in SQL Server and this topic is not related to SQL Server Transaction in any way.
Background
Basic knowledge of Command Pattern is required.
Using the Code
First, what is Transaction?
In simplest terms, a Transaction must exhibit the following behaviour.
"Each transaction must succeed or fail as a complete unit; it can never be only partially complete."
Source: Wikipedia.
So to implement transaction in your class library, we will use the command pattern because it is this pattern which provides the way to implement not only the transaction, but also other features too like logging the operations, undo/redo, recovering from the crash, etc.
So let’s start, there is a sample solution attached, in which two examples are given, one is Console based, it explains the basic transaction and then WinForm application in which that transaction knowledge is applied into the real world scenario.
Let’s start.
Our First Interface in 'Transaction Enabled Architecture'
public interface ITransactionHandler
{
void Rollback();
bool IsExecuted { get; }
}
This interface is to be implemented by Receiver which has to provide the transaction support so that it can participate in transaction because without touching the Receiver, a transaction cannot be supported.
Note: We can merge the 'ICommand'
and 'ITransactionHandler'
too in one interface of 'ICommand'
but we have not done this because if it a very large application and our 'ICommand'
is being used in other projects too but those project may not be using Transaction
and may not need to provide the 'Rollback' implementation. Therefore it is separated into Transaction part and it provide more flexibility.
Our Second Interface
public interface ITransaction
{
bool Executed();
void Rollback();
}
This interface is to be implemented by the ReceiverCommand
so that TransactionService
(explained below) can know that ReceiverCommand
also supports Transaction not just Receiver.
public interface ITransactionService : IDisposable
{
ICommand Command { set; }
void Commit();
void Rollback();
}
public class TransactionService : ITransactionService
{
private Queue<ITransaction> _commandQueue = new Queue<ITransaction>();
private TransactionState IsTransactionCommitted = TransactionState.None;
public ICommand Command
{
set
{
if (value is ITransaction)
{
this._commandQueue.Enqueue((ITransaction)value);
}
else
{
throw new NotSupportedException("Transaction is not supported");
}
}
}
public void Commit()
{
if (this.IsTransactionCommitted == TransactionState.Committed)
{
throw new Exception("Transaction is already committed");
}
else if (this.IsTransactionCommitted == TransactionState.Rollbacked)
{
throw new Exception("Transaction is already rollbacked");
}
else
{
this.IsTransactionCommitted = TransactionState.Committed;
this._commandQueue.Clear();
}
}
public void Rollback()
{
if (this.IsTransactionCommitted == TransactionState.Committed)
{
throw new Exception("Transaction is already committed");
}
else if (this.IsTransactionCommitted == TransactionState.Rollbacked)
{
throw new Exception("Transaction is already rollbacked");
}
else
{
while (this._commandQueue.Count > 0)
{
ITransaction transaction = this._commandQueue.Dequeue();
if (transaction.Executed())
{
transaction.Rollback();
}
}
this.IsTransactionCommitted = TransactionState.Rollbacked;
}
}
public void Dispose()
{
if (this.IsTransactionCommitted == TransactionState.None)
{
while (this._commandQueue.Count > 0)
{
ITransaction transaction = this._commandQueue.Dequeue();
if (transaction.Executed())
{
transaction.Rollback();
}
}
this.IsTransactionCommitted = TransactionState.Rollbacked;
}
this._commandQueue = null;
}
private enum TransactionState
{
None,
Committed,
Rollbacked
}
}
This interface and its implemented class is the heart of the transaction and it is used by calling application to create an instance of transaction object and perform commit operation on successful execution and call rollback method on failure of execution.
This is a calling application's code which calls commands in a transaction scope.
using (ITransactionService transactionService = new TransactionService())
{
ICommand firstCommand = new ReceiverCommand(new Receiver());
ICommand secondCommand = new ReceiverCommand(new Receiver());
transactionService.Command = firstCommand;
transactionService.Command = secondCommand;
try
{
Console.WriteLine("Starting Transaction...");
firstCommand.Execute();
if (input == 'Y' || input == 'y')
{
throw new Exception("Reason for failure to execute second command");
}
secondCommand.Execute();
transactionService.Commit();
Console.WriteLine();
Console.WriteLine("Transaction Successful Executed...");
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("Exception Occured: " + ex.Message);
Console.WriteLine("Due to exception Transaction is rolledback");
transactionService.Rollback();
}
}
First transaction
object is created in using
block so that its Dispose
method can be called. Then two different receiver objects are created and we set these commands to transaction
object so that they can participate in transaction. Once they are successfully executed, the transaction object is committed otherwise it is rolled back.
Note: The way TransactionService
object is created in 'using'
block and on successful execution commit must be called or if it is not called, then on Dispose
RollBack will occur. I have tried to follow the 'Microsoft implementation of implicit transaction model'.
Screenshot for Console Based Example
To better understand the transaction, first go through the Console based application in which the core concept is explained and after understanding it, look into the example of WinForm based application.
Screenshot for WinForm Based Example
In WinForm based applications, when you execute the code, it will load the country name from XML file, when you double click any node in treeview
, it adds the country name to cache list, and when you check the 'Should Exception Throw in TextBox' checkbox, then you click the 'Perform Operation' button then exception will occur 'TextBox Receiver' while performing the operation, if 'Is Transaction Enabled' checkbox is checked, then no operation will be succeeded otherwise you will see that first operation is executed successfully but not second operation. Please have a look into this code to better understand the transaction.
Points of Interest
How can I take this transaction architecture to support in distributed application ? This will be interesting to see.
I tried my best to explain the transaction with both the examples, but feedback is most welcome so that I can improve the architecture of transaction.
History
- 13th October, 2014 - First draft version
- 20th October, 2014 - Second draft version
- Article content is updated.
- '
ITransaction
' interface method's name changed to reflect more what it does.
- '
Dispose
' method of TransactionService
is changed.
Private Enum
'TransactionState
' is added in TransactionService
class