5. Using a Resource Manager in a Successful Transaction
Using the RM in transactional code is really simple. You just wrap it in a TransactionScope, as shown here:
var vrm = new VolatileRM("RM1");
Console.WriteLine("Member Value:" + vrm.MemberValue);
using (var tsc = new TransactionScope())
{
vrm.MemberValue = 3;
tsc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
When you run this code, you should see the following output indicating that the resource manager participated in a successful transaction:
Member Value: 0
RM1: MemberValue setter – EnlistVolatile
RM1: Prepare
RM1: Commit
Member Value: 3
As you can see, the RM enlists in the prepare and commit phases of the two-phase commit process.
Using the Resource Manager When the Caller Issues a Rollback
Now you’ll modify the code. Comment out the tsc.Complete()
statement and run the application again. You should see the following
output, indicating that the resource manager participated in a
transaction that was rolled back:
Member Value: 0
RM1: MemberValue setter - EnlistVolatile
RM1: Rollback
Member Value: 0
By commenting out the tsc.Complete() statement, you are simulating a condition in which the application that uses the RMs enforces a rollback.
Tip
It’s best practice to always place the tsc.Complete() statement as the very last line of code within your TransactionScope blocks. Doing so ensures that it won’t get executed if an exception occurs anywhere within the block.
Instead of managing the prepare and commit phases, the code instead reacts to the rollback phase, and the final value of the member variable is unchanged from the original value.
Using the Resource Manager When It Issues a Rollback
Now go ahead and put tsc.Complete back in the code and modify the Prepare method of the RM. Comment out the Prepared method call and put in a ForceRollBack call instead, as follows:
public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine(_whoAmI + ": Prepare");
// preparingEnlistment.Prepared();
preparingEnlistment.ForceRollback();
}
The RM now issues a rollback. When you execute the application with tsc.Complete in place, a TransactionAbortedException exception is thrown because the resource manager itself issued a rollback.
Using the Resource Manager with Another Resource Manager
Now you’ll restore the Prepare
method of the RM back to its original state so that a rollback isn’t
issued. Back in the host application, modify the original code to
include a second RM participating in the same transaction, as shown
here:
var vrm = new VolatileRM("RM1");
var vrm2 = new VolatileRM("RM2");
Console.WriteLine("Member Value 1:" + vrm.MemberValue);
Console.WriteLine("Member Value 2:" + vrm2.MemberValue);
using (var tsc = new TransactionScope())
{
...
vrm.MemberValue = 3;
vrm2.MemberValue = 5;
tsc.Complete();
}
Console.WriteLine("Member Value 1:" + vrm.MemberValue);
Console.WriteLine("Member Value 2:" + vrm2.MemberValue);
As you can see, the code simply enlists another instance of the RM being used in the same transaction. When this code is executed, the following output indicates that two instances of the resource manager worked independently within a single transaction:
Member Value: 0
Member Value: 0
RM1: MemberValue setter - EnlistVolatile
RM2: MemberValue setter - EnlistVolatile
RM1: Prepare
RM2: Prepare
RM1: Commit
RM2: Commit
Member Value: 3
Member Value: 5
As
you can see, when multiple RMs are involved in the transaction, the
appropriate prepare, commit, or rollback phases are called for each RM
in succession. As an exercise, you could modify the RM code to include
a ForceRollBack and see the succession of events if one of the RMs issues a rollback to the entire transaction.
We’ve saved the best part for last. Remember that SqlConnection is also an RM, so you can retry this experiment with an instance of SqlConnection, a SqlCommand, and a database query executed within the same transaction that VolatileRM is enlisted in. To do so, modify the code to match this:
var vrm = new VolatileRM("RM1");
Console.WriteLine("Member Value:" + vrm.MemberValue);
const string connStr =
"Data Source=(local);Initial Catalog=Test1;Integrated Security=SSPI;";
const string cmdText = "UPDATE FromTable SET Amount = Amount - 50";
using (var tsc = new TransactionScope())
{
vrm.MemberValue = 3;
using (SqlConnection conn1 = new SqlConnection(connStr))
{
SqlCommand cmd1 = conn1.CreateCommand();
cmd1.CommandText = cmdText;
conn1.Open();
cmd1.ExecuteNonQuery();
}
tsc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
By doing so, you would note that your VolatileRM now participates in the same transaction that a database query has enlisted itself in. This is something that BEGIN DISTRIBUTED TRANSACTION
cannot do because by its very nature it talks to database queries,
which cannot perform nondatabase operations. Unless, that is, you are
using SQL CLR, which is where things can get a bit blurry.