Introduction
In every project we need a business transaction class which help us
to control our business flow. This class should be able to handle
nested transactions too. I wrote a class, named "BusinessTransaction"
and want to share it here with every one who has problem with nested
transaction handling.
Background
In many situations we have some Services/Facades/Managers which
process data containers and save them in database. Sometime these
services call each other too, in these cases if any exception occur in
inner service, the exception is thrown in a 2 level transaction and
both of them should be rolled back.
For example, in an algorithm like this,
public void X()
{
try
{
transaction.Start();
console.WriteLine("X");
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw ex;
}
}
public void Y()
{
try
{
transaction.Start();
console.WriteLine("Y");
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw ex;
}
}
public void Z()
{
try
{
transaction.Start();
X();
Y();
transaction.Commit();
}
catch(Exception ex)
{
transaction.Rollback();
throw ex;
}
}
We can have some cases such as:
1- An Exception occurs between X transaction: X and Z should be rolled back.
2- An Exception occurs after X commit and between Y transaction: X,Y and Z should be rolled back.
3-
An Exception occurs After X commit and between Y transaction, but Y
doesn't throw it: Only Y should be rolled back, X and Z must be
committed.
4- An Exception occurs After Y commit: X,Y and Z should be rolled back.
So, i tried to handle all of these manners in my code and tested them; it worked!
I always use LLBLGen Pro. as ORM and of course you can see it in my
code, but if you are not familiar with it, don't worry, the related
code is easy to understand!
Using the code
I
used a stack to handle nested transactions. In every Start() Method,
the related transaction identifier is pushed in it, only if the stack
is empty, it means that no outer transaction exists, so i starts a real
transaction.
public void Start(IsolationLevel isolationLevel)
{
string startMethodName = GetSavePoint();
int transactionID = tarnsactionIDToInfoMap.Count + 1;
if (transactionStack.Count == 0)
{
adapter.StartTransaction(isolationLevel, "1");
tarnsactionIDToInfoMap.Clear();
}
else
{
adapter.SaveTransaction(transactionID.ToString());
}
tarnsactionIDToInfoMap.Add(transactionID, startMethodName);
transactionStack.Push(transactionID);
}
When commit() or rollback() are called, one transaction is popped, and
again if stack becomes empty, it means that the transaction is a real
one and have to be rolled back or committed.
public void Commit()
{
if (transactionStack.Count == 0)
{
throw new FatalException("Error in transaction Handling. Commit on empty stack", true);
}
string commitMethodName = GetSavePoint();
string srartMethodName = tarnsactionIDToInfoMap[(int)(transactionStack.Pop())].ToString();
if (!commitMethodName.Equals(srartMethodName))
{
LogManager.Instance.WriteWarning("Unmatched transaction. " +
"The Start Transaction was called from " + srartMethodName +
" but it was commited in " + commitMethodName);
}
if (transactionStack.Count == 0)
{
adapter.Commit();
}
}
public void Rollback()
{
if (transactionStack.Count == 0)
{
throw new FatalException("Error in transaction Handling. Rollback on empty stack", true);
}
int transactionID = (int)(transactionStack.Pop());
string rollbackMethodName = GetSavePoint();
string startMethodName = tarnsactionIDToInfoMap[transactionID].ToString();
if (!rollbackMethodName.Equals(startMethodName))
{
LogManager.Instance.WriteWarning("Unmatched transaction. " +
"The Start Transaction was called from " + startMethodName +
" but it was rolled back in " + rollbackMethodName);
}
if (transactionStack.Count == 0)
{
adapter.Rollback();
}
else
{
adapter.Rollback(transactionID.ToString());
}
}
I hope this class be useful!