Friday, August 22, 2008

Transactions in asp.net 2.0

New transaction model addresses shortcomings
The Microsoft .NET 2.0 platform introduces a new namespace called System.Transactions that brings in a lightweight, simplified transaction model. This article discusses the shortcomings of current transaction models and introduces System.Transactions for .NET Framework 2.0 beta 2.
Currently there are two transaction models available in the .NET world, both of which are suited for certain situations. The first is part of the rich ADO.NET data providers. The second is the enterprise services transaction. Before we examine System.Transactions, let’s have a look at the current models.
The ADO.NET transaction model
The transaction handling model available as part of the .NET data providers is very simple. A general update statement might consist of the following steps:
C# Code
SqlConnection con = new SqlConnection("Connection String");
SqlTransaction tr = con.BeginTransaction();
SqlCommand cmd = new SqlCommand("Update Account set Balance=500 where AccountId=52", con, tr);
try
{
cmd.ExecuteNonQuery();
tr.Commit();
}
catch (Exception exc)
{
tr.Rollback();
}
finally
{
con.Close();
}
We created a transaction with the connection object and associated the transaction objects with the command objects. After executing the commands, we specified whether the transaction should commit or rollback. If there is an exception, we select a rollback, and if not, we are committing the transaction.
Though the ADO.NET transaction seems like a good model, there are a few problems associated with it. One of the biggest drawbacks occurs when you have updates for more than one database or resource grouped under a single transaction. As you can see in the previous code block, the transaction object is actually created from a connection to a single database. So there is no direct way of grouping updates to more than one database into a single transaction.
Also, when you manage transactions in an object-oriented scenario, database transactions are not ideal. In an object-oriented environment, many objects (such as customer, invoice number, etc.) coordinate together to complete a business process. If the business process is transactional, the transaction object might need to be passed around or there might be additional code involved to create a layer to manage the process.
Enterprise services transactions
Enterprise services transactions address most of the shortcomings of the ADO.NET transactions by providing a two-phase commit protocol and a distributed transaction manager. These two features enable you to have transactions independent of the database, and provide transactions in a declarative manner.
The following code shows a simple AccountManager class that accepts two accounts and performs a transfer between the two. The transfer is completed by depositing cash into one account and withdrawing cash from the other, with each method separately updating the balances of the accounts in the database. If any of these methods generates an error, the entire transfer method will be rolled back.
C# Code
[Transaction(TransactionOption.Required)]
class AccountManager : ServicedComponent
{
[AutoComplete()]
void TransferCash(Account from, Account to, double amt)
{
from.Withdraw(amt);
to.Deposit(amt);
}
}
If you look at the above code block, we are not explicitly creating any transaction objects. We are using declarative transactions to mark areas where transactions will be used. The Autocomplete attribute specifies that the transaction should be commited if no errors are generated. The Transaction attribute paired with TransactionOption.Required specifies that the objects we create from AccountManager will be transactional and will always run under a transaction.
With this model, you can access more than one resource and enlist them in one transaction. In the above code, for example, the withdrawal method can update one database and the deposit method can update another database.
But there are still a few problems in this model. First, your class needs to inherit from the ServicedComponent class to take advantage of enterprise service transactions. With the single inheritance model, most .NET languages will restrict your class from inheriting from any other base class.
Another drawback is that enterprise service transactions are always taken as a distributed transaction. Even if you are using a single database, enterprise services will still take it as a distributed transaction and handle it as such. As a result, you might end up using more resources than needed.
Another problem is that the components you create need to be deployed in component services to run under the COM+ context. Though .NET simplifies this deployment a great deal, you still need to place a strong name for the assembly with serviced components and make sure the settings for the components are set properly.
Introducing System.Transactions
The System.Transactions model is a new addition to the .NET 2.0 framework. It addresses the shortcomings in the above discussed models, and brings the best features of the ADO.NET and enterprise services transaction models together.
In System.Transactions, you have the TransactionScope object you can use to scope a set of statements and group them under one transaction. The following System.Transactions model generates the same transaction we saw in the enterprise services example.
C# Code
class AccountManager
{
void TransferCash(Account from, Account to, double amt)
{
using(TransactionScope scope=new TransactionScope())
{
from.Withdraw(amt);
to.Deposit(amt);
scope.Complete();
}
}
}
If you look at the above code, our class is not inheriting any base classes to enable transactions. All we are doing is using a TransactionScope object and wrapping the method calls accessing the database with a using block. Once complete, we are setting the scope to complete and indicating the transaction should commit now. But what if the withdrawal method creates an exception? Then the complete method for the scope will not be called and the transaction will be rolled back.
System.Transactions has the unique capability of knowing whether to use a distributed transaction or not. It will use a lightweight transaction if there is a single domain accessing a single database, or it will use a distributed transaction similar to the enterprise services transaction model if there are multiple databases to access.
In most scenarios, you will interact with the TransactionScope object for your transaction handling work. You can even nest transaction scopes and provide transaction options to specify how each of the blocks interact with each other. Below we have a method to log successful withdrawals and we need to make sure this method will attach itself to a root transaction if it exists or spawn a new transaction otherwise:
C# Code
void LogWithdrawals(Account account,double amt)
{
using (TransactionScope scope = new TransactionScope (TransactionScopeOption.Required))
{
//database calls to log details
}

}
In the above code, we have created a transaction scope option with the value Required. This is used to specify that the block of code should always be within a transaction. If there is an existing transaction, the scope within LogWithdrawals will join with the root transaction; otherwise it will create a new one. The other transaction scope options available are RequiresNew (will always create a new transaction) and Suppress (will never be part of a transaction). These options can be used to change the way nested transactions will participate in the overall transaction.
You can also adorn a transaction scope with a TransactionOption object to specify the isolation level and the timeout period of the transaction. For example:
C# Code
TransactionOptions options = new TransactionOptions();
options.IsolationLevel = IsolationLevel.ReadCommitted;
options.Timeout = new TimeSpan(0, 1, 0);
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew,options))
{
//code within transaction
}
The above code sets our transaction scope to use an isolation level of read committed; we have also set a timeout of one minute for our transaction.