Introduction
Transaction class is the most common class using
Scope pattern to handle transactions in a clean and easy nestable way.
It uses the same strategy as Microsoft's
TransactionScope from the client code point of view: Implicit transaction in the transaction scope defined by the
using statement.
Internally, however, it has some differences, let's take a look:
How to use it
Some time ago this code tended to be very cumbersome. Implicit transactions, however, made a big step forward, creating a easy model to handle transactions writing less code:
private static void FixBugs()
{
var wrongBugs = from b in Database.Query<BugDN>()
where b.Status == Status.Fixed && !b.End.HasValue
select b.ToLazy();
foreach (var lazyBug in wrongBugs)
{
BugDN bug = lazyBug.Retrieve();
bug.Description += "- Fix it!";
bug.Save();
}
}
This simple code contains a lot of intensive work with the database: the query, retrieving each Bug, and saving it. Currently all of these are independent operations, so we could have some problems:
- If some exception is thrown in the middle of the process we end up having some bugs fixed and some remaing. This could have some problems, like duplicating - Fix It! for some bugs once you r-run the process.
- It could take some time from the time the query gets executed, to the moment the BugDN is actually retrieved (we are using lazy objects). Someone could have modified the Bug in the meantime.
In Signum Engine, every atomic operation, like retrieving an object, executing a query, or saving an object graph is transactional without you having to do anything.
Sometimes, however, you want to glue together operations in the same transaction, so you have a consistent view of the database (not influenced by concurrency) and you don't commit partial modifications if something goes wrong in the middle.
This is as easy as surrounding the code with a suing Transaction block, like this:
private static void FixBugs()
{
using (Transaction tr = new Transaction())
{
var wrongBugs = from b in Database.Query<BugDN>()
where b.Status == Status.Fixed && !b.End.HasValue
select b.ToLazy();
foreach (var lazyBug in wrongBugs)
{
BugDN bug = lazyBug.Retrieve();
bug.End = bug.Start.AddDays(7);
bug.Save();
}
tr.Commit();
}
}
What we do is to create a transaction, do our work, and at the end Commit it. When the transaction is disposed, if it hasn't been committed, it gets roll backed automatically, so the code gets much more simple to read.
Be careful with return statements, remember to commit the transaction before exit the method!
The nice thing is that all the old code doesn't need to be updated with the new declared Transaction tr, because transactions are handled implicitly.
Also, if you call FixBigs from a method like this:
private static void FixTheWorld()
{
FixBugs();
FixCustomers();
FixDevelopers();
}
You cold also glue together all the inner transactions creating a new, wider one:
private static void FixTheWorld()
{
using (Transaction tr = new Transaction())
{
FixBugs();
FixCustomers();
FixDevelopers();
tr.Commit();
}
}
The end result is that the inner transactions become silent without you having to change anything, so the code is
much more easy to combine.
There are some options for useful overloadings of Transaction constructor:
public Transaction()
public Transaction(bool forceNew)
public Transaction(IsolationLevel isolationLevel)
public Transaction(bool forceNew, IsolationLevel? isolationLevel)
.
How it works (AdvancedTopic)
There are some things that are important to know in advanced scenarios:
- Each time you force a new transaction, a RealTransaction is pushed into the transaction stack. There are different stacks per Thread and Connection.
- Transaction knows the start time of the transaction, that is used to update the Entity Ticks column for concurrency control.
- Transactions are completely silent on MockConnections
- Transaction takes control of SqlClient SqlTransaction and SqlConnection. You can access them on
Transaction.CurrentConnection and Transaction.CurrentTransaccion (they will be created if not created yet).
Probably you where expecting
Connection to handle SqlConnection, but Connection is more like a potential connection than an actual one, and it's shared between threads. Transaction has better knowledge about when to start the connection and how long to keep it open.