Generated code - Transactions, Adapter

Preface

Adapter supports, like SelfServicing, both COM+ and ADO.NET transactions. Below is explained how ADO.NET transactions are used in Adapter, and how COM+ transactions can be used in Adapter. COM+, or better: enterprise services, offers more than just transactional behavior. You can also use COM+ to enable Just-In-Time activation (JIT) or object pooling. The COM+ transaction section below has a small discussion on that as well. Adapter automatically uses ADO.NET transactions for recursive saves and save/delete actions for collections of entities, unless the adapter object is owned by a ComPlusAdapterContext object.

Normal native database transactions

(It's assumed the database used supports transactions, which is the case in all major databases like SqlServer). Native database transactions are provided by ADO.NET; it's a part of an ADO.NET connection object. Database statements can be executed within the same transaction by using the same connection object. LLBLGen Pro's native database transactions are implemented in the DataAccessAdapter object. You can simply start a transaction using a DataAccessAdapter object, execute methods of that DataAccessAdapter object and rollback or commit the transaction. Because the transactional code is inside the DataAccessAdapter, every method of the DataAccessAdapter object you call after you've started a transaction is ran inside that transaction, including stored procedure calls, entity fetches, entity collection saves etc. This greatly simplifies the programming of code using the variety of functionality the DataAccessAdapter object offers. You don't have to add entity objects or entity collection objects to the DataAccessAdapter object to make them participate in the transaction, just call a DataAccessAdapter method and it's inside the transaction of that particular DataAccessAdapter object. If you wish to run a particular action outside of a transaction, create a new DataAccessAdapter object for that particular action.

note Note:
If you start a new transaction by calling StartTransaction() the connection is kept open until Rollback() or Commit() is called.

An example will help illustrate the usage of the transaction functionality of the DataAccessAdapter object. We're going to update 2 different entities in one transaction. For this example, it has to be done in one go, as an atomic unit, and therefore requires a transaction. The data is rather bogus, it's for illustration purposes only).

// [C#]
// create adapter for fetching and the transaction. 
DataAccessAdapter adapter = new DataAccessAdapter();
// start the transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "TwoUpates");
try
{
	// fetch the two entities
	CustomerEntity customer = new CustomerEntity("CHOPS");
	OrderEntity order = new OrderEntity(10254);
	adapter.FetchEntity(customer);
	adapter.FetchEntity(order);
	
	// alter the entities
	customer.Fax = "12345678";
	order.Freight = 12;
	
	// save the two entities again.
	adapter.SaveEntity(customer);
	adapter.SaveEntity(order);
	
	// done
	adapter.Commit();
}
catch
{
	// abort, roll back the transaction
	adapter.Rollback();
	// bubble up exception
	throw;
}
finally
{
	// clean up. Necessary action.
	adapter.Dispose();
}
' [VB.NET]
' create adapter for fetching and the transaction. 
Dim adapter As New DataAccessAdapter()
' start the transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "TwoUpates")

Try
	' fetch the two entities
	Dim customer As New CustomerEntity("CHOPS")
	Dim order As New OrderEntity(10254)
	adapter.FetchEntity(customer)
	adapter.FetchEntity(order)
	
	' alter the entities
	customer.Fax = "12345678"
	order.Freight = 12
	
	' save the two entities again.
	adapter.SaveEntity(customer)
	adapter.SaveEntity(order)
	
	' done
	adapter.Commit()
Catch
	' abort, roll back the transaction
	adapter.Rollback()
	' bubble up exception
	Throw
Finally
	' clean up. Necessary action.
	adapter.Dispose()
End Try

First a DataAccessAdapter object is created and a transaction is started. As soon as you start the transaction, a database connection is open and usable. This is also the reason why you must include a final clause and call Dispose() when the DataAccessAdapter object is no longer needed. This is good practice anyway. The code is the same as if you didn't start a transaction, except for the Commit() / Rollback() combination at the end. Simply start a transaction, call the methods you want to call, and if no exceptions are caught, Commit(), otherwise Rollback().

It's best practice to embed the usage of a transaction in a try/catch/finally statement as it is done in the example above. This ensures that if something fails during the usage of the transaction, everything is rolled back or, at the end, everything is committed correctly.

Transaction savepoints

Most databases support transaction savepoints. Transaction savepoints make it possible to do fine grained transaction control on a semi-nested level. This can be required as ADO.NET doesn't support nested transactions. Savepoints let you define a point in a transaction to which you can roll back, without rolling back the complete transaction. This can be handy if you have saved some entities in a transaction which were saved OK, and another one fails, however the failure of that save shouldn't terminate the whole transaction, just roll back the transaction to a given point in the transaction. LLBLGen Pro offers you the ability to define savepoints in a transaction. The following example illustrates the savepoint functionality. It first saves a new address entity and after that it saves the transaction. It then saves a new customer entity but takes into account that this can fail. If it does, it should roll back to the savepoint set, it should thus not rollback the complete transaction. Consider the example an illustration for the feature, in your code, the code utilizing the transaction will probably span several classes and methods. Savepoints are not supported with COM+ transactions.

//C#
DataAccessAdapter adapter = new DataAccessAdapter();

try
{
	adapter.StartTransaction(IsolationLevel.ReadCommitted, "SavepointRollback");

	// first save a new address
	AddressEntity newAddress = new AddressEntity();
	// ... fill the address entity with values

	// save it.
	adapter.SaveEntity(newAddress, true);

	// save the transaction
	adapter.SaveTransaction("SavepointAddress");

	// save a new customer
	CustomerEntity newCustomer = new CustomerEntity();
	// ... fill the customer entity with values
	newCustomer.VisitingAddress = newAddress;
	newCustomer.BillingAddress = newAddress;
	
	try
	{
		adapter.SaveEntity(newCustomer, true);
	}
	catch(Exception ex)
	{
		// something was wrong. 
		// ... handle ex here.

		// roll back to savepoint.
		adapter.Rollback("SavepointAddress");
	}
	
	// commit the transaction. If the customer save failed, 
	// only address is saved, otherwise both.
	adapter.Commit();
}
catch
{
	// fatal error, roll back everything
	adapter.Rollback();
	throw;
}
finally
{
	adapter.Dispose();
}
' VB.NET
Dim adapter As new DataAccessAdapter()

Try
	adapter.StartTransaction(IsolationLevel.ReadCommitted, "SavepointRollback")

	' first save a new address
	Dim newAddress As New AddressEntity()
	' ... fill the address entity with values

	' save it.
	adapter.SaveEntity(newAddress, True)

	' save the transaction
	adapter.SaveTransaction("SavepointAddress")

	' save a new customer
	Dim newCustomer As New CustomerEntity()
	' ... fill the customer entity with values
	newCustomer.VisitingAddress = newAddress
	newCustomer.BillingAddress = newAddress
		
	Try
		adapter.SaveEntity(newCustomer, True)
	Catch(Exception ex)
		' something was wrong. 
		' ... handle ex here.

		' roll back to savepoint.
		adapter.Rollback("SavepointAddress")
	End Try
	
	' commit the transaction. If the customer save failed, 
	' only address is saved, otherwise both.
	adapter.Commit()
Catch
	// fatal error, roll back everything
	adapter.Rollback()
	Throw
Finally
	adapter.Dispose()
End Try

note Note:
Microsoft Access doesn't support savepoints in transactions, so this feature is not supported when you use LLBLGen Pro with MS Access.

COM+ transactions

LLBLGen Pro supports COM+ transactions for adapter as well through a special COM+ class called ComPlusAdapterContext. This class is generated in the DataAccessAdapter class file and is thus located in the database specific project. The ComPlusAdapterContext class is actually a thin wrapper class which implements abstract methods in its base class ComPlusAdapterContextBase. The ComPlusAdapterContext class embeds a DataAccessAdapter object which will act the same as a normal DataAccessAdapter object, only this time it will utilize the COM+ context held by the ComPlusAdapterContext instance. Because the ComPlusAdapterContext class takes care of the database connection creation, the COM+ context held by the ComPlusAdapterContext will make sure that any database activity which uses the connection objects created by the ComPlusAdapterContext class are monitored by COM+ (the MS DTC service).

The ComPlusAdapterContext class is just one example how COM+ can be used together with Adapter. You can also create your own class, deriving from ComPlusAdapterContextBase and add different EnterpriseServices attributes to it to enable different services offered by COM+, like Just-In-Time activation or object pooling. Also you can for example grab the connection string from the COM+ object definition defined in Windows' Component Services.

Because COM+ is implemented in .NET using Enterprise Services, the class using the ComPlusAdapterContext object has to derive from ServicedComponent. This way, transactions started outside the class using the ComPlusAdapterContext class can flow through to the ComPlusAdapterContext class to the actions performed by the DataAccessAdapter object inside the ComPlusAdapterContext object. Also, you have to reference the System.EnterpriseServices namespace in your code. Below is a short example how a COM+ transaction flows through to a ServicedComponent derived class and the code inside the TestComPlus() method will be running inside the COM+ transaction. When no COM+ transaction is available, a new one is created.

// [C#]
[Transaction(TransactionOption.Required)]
public class TestClass : ServicedComponent
{
	[AutoComplete]
	public void TestComPlus()
	{
		ComPlusAdapterContext comPlusContext = new ComPlusAdapterContext();
		IDataAccessAdapter adapter = comPlusContext.Adapter;

		try
		{
			// start transaction. 
			adapter.StartTransaction(IsolationLevel.ReadCommitted, "ComPlusTran");

			CustomerEntity customer = new CustomerEntity("CHOPS");
			OrderEntity order = new OrderEntity(10254);
			adapter.FetchEntity(customer);
			adapter.FetchEntity(order);
			
			// alter the entities
			customer.Fax = "12345678";
			order.Freight = 12;
			
			// save the two entities again.
			adapter.SaveEntity(customer);
			adapter.SaveEntity(order);
			
			// done
			adapter.Commit();
		}
		catch
		{
			// abort
			adapter.Rollback();
			throw;
		}
		finally
		{
			comPlusContext.Dispose();
			((DataAccessAdapter)adapter).Dispose();
		}
	}
}
' [VB.NET]
<Transaction(TransactionOption.Required)> _
Public Class TestClass 
	Inherits ServicedComponent

	<AutoComplete> _
	Public Sub TestComPlus()
		Dim comPlusContext As New ComPlusAdapterContext()
		Dim adapter As IDataAccessAdapter = comPlusContext.Adapter

		Try
			' start transaction. 
			adapter.StartTransaction(IsolationLevel.ReadCommitted, "ComPlusTran")

			Dim customer As New CustomerEntity("CHOPS")
			Dim order As New OrderEntity(10254)
			adapter.FetchEntity(customer)
			adapter.FetchEntity(order)
			
			' alter the entities
			customer.Fax = "12345678"
			order.Freight = 12
			
			' save the two entities again.
			adapter.SaveEntity(customer)
			adapter.SaveEntity(order)
			
			' done
			adapter.Commit()
		Catch
			' abort
			adapter.Rollback()
			Throw
		Finally
			comPlusContext.Dispose();
			((DataAccessAdapter)adapter).Dispose();
		End Try
	End Sub
End Class

note Note:
COM+ transactions are considered 'advanced material' in .NET applications. Use them with care. You have to give your assemblies a strong name and your application will cause extra overhead on your machine: every serviced component has a context in the COM+ service. Most of the time you can fulfill your transactional requirements using native database transactions with the normal ADO.NET transactions provided by the DataAccessAdapter, as illustrated in the previous section.

.NET 2.0: System.Transactions support

.NET 2.0 introduces the System.Transactions namespace. This is a namespace with the TransactionScope class, which eases the creation of distributed transactions, specifying a given scope. All transactions, for example normal ADO.NET transactions, are automatically elevated to distributed transactions, if required, by the TransactionScope they're declared in. This requires support by the used database system as the database system has to be able to promote a non-distributed transaction to a distributed transaction. When .NET 2.0 shipped, only SqlServer 2005 was able to promote transactions to distributed transactions using System.Transactions' classes.

The developer can define such a TransactionScope using the normal .NET constructs, like

using(TransactionScope scope = new TransactionScope())
{
	// your code here.
}



A DataAccessAdapter object is able to determine if it's participating inside an ambient transaction of System.Transactions. If so, it enlists a Resource Manager with the System.Transactions transaction. The Resource manager contains the DataAccessAdapter object. As soon as a Transaction or DataAccessAdapter is enlisted through a Resource Manager, the Commit() and Rollback() methods are setting the ResourceManager's commit/abort signal which is requested by the System.Transactions' Transaction manager. If multiple transactions are executed on a DataAccessAdapter and one rolled back, the resource manager will report an abort. As soon as the DataAccessAdapter is enlisted in the System.Transactions.Transaction, no ADO.NET transaction is started, it's a no-op. Once one rollback is requested, the transaction will always report a rollback to the MSDTC.
Going out of scope
When the System.Transactions transaction is committed or rolled back, the Resource manager is notified and will then notify the DataAccessAdapter that it can commit/rollback the transaction. That call will then notify the enlisted entities of the outcome of the transaction.
Multiple transactions executed using a single DataAccessAdapter object
For the DataAccessAdapter it will look like its still inside the same transaction, so no new transaction is started. This will make sure that an entity which is already participating in the transaction isn't enlisted again and the field values aren't saved again etc.
Example
Below is an example which shows the usage of a TransactionScope in combination of a DataAccessAdapter object. The code contains Assert statements to illustrate the state / outcome of the various statements.

 // C#
CustomerEntity newCustomer = new CustomerEntity();
// fill newCustomer's fields.
// ..
AddressEntity newAddress = new AddressEntity();
// fill newAddress' fields.
// ..

// start the scope.
using( TransactionScope ts = new TransactionScope() )
{
	// as we're inside the transaction scope, we can now create a DataAccessAdapter object and
	// start a connection + transaction. The connection + transaction will be enlisted through a 
	// resource manager in the TransactionScope ts and will be controlled by that TransactionScope.
	using(DataAccessAdapter adapter = new DataAccessAdapter())
	{
		// save 2 entities non-recursive. This should be done in one 
		// transaction, namely the transaction scope we've started. 
		newCustomer.VisitingAddress = newAddress;
		newCustomer.BillingAddress = newAddress;
		Assert.IsTrue( adapter.SaveEntity( newCustomer, true) );
		
		// save went well, alter the entities, which are fetched back, and 
		// save again.
		newCustomer.CompanyEmailAddress += " ";
		newAddress.StreetName += " ";
		Assert.IsTrue( adapter.SaveEntity( newCustomer, true ) );
	}
	// do not call Complete, as we want to rollback the transaction and see if the rollback indeed succeeds.
	// as the TransactionScope goes out of scope, the on-going transaction is rolled back.
}
// at this point the transaction of the previous using block is rolled back.
// let the DTC and the system.transactions threads deal with the objects. 
// this sleep is only needed because we're going to access the data directly after the rollback. In normal code, 
// this sleep isn't necessary.
Thread.Sleep( 1000 );

// test if the data is still there. Shouldn't be as the transaction has been rolled back. 
using( DataAccessAdapter adapter = new DataAccessAdapter() )
{
	CustomerEntity fetchedCustomer = new CustomerEntity( newCustomer.CustomerId );
	Assert.IsFalse( adapter.FetchEntity( fetchedCustomer ) );
	AddressEntity fetchedAddress = new AddressEntity( newAddress.AddressId );
	Assert.IsFalse( adapter.FetchEntity( fetchedAddress ) );
	Assert.AreEqual( 0, newAddress.AddressId );
}
 ' VB.NET
Dim NewCustomer As New CustomerEntity()
' fill NewCustomer's fields.
' ..
Dim NewAddress As New AddressEntity()
' fill NewAddress' fields.
' ..

' start the scope.
Using  ts As New TransactionScope() 
	' as we're inside the transaction scope, we can now create a DataAccessAdapter object and
	' start a connection + transaction. The connection + transaction will be enlisted through a 
	' resource manager in the TransactionScope ts and will be controlled by that TransactionScope.
	Using adapter As New DataAccessAdapter()
		' save 2 entities non-recursive. This should be done in one 
		' transaction, namely the transaction scope we've started. 
		NewCustomer.VisitingAddress = NewAddress
		NewCustomer.BillingAddress = NewAddress
		Assert.IsTrue( adapter.SaveEntity( NewCustomer, True) )
		
		' save went well, alter the entities, which are fetched back, and 
		' save again.
		NewCustomer.CompanyEmailAddress = NewCustomer.CompanyEmailAddress & " "
		NewAddress.StreetName = NewAddress.StreetName & " "
		Assert.IsTrue( adapter.SaveEntity( NewCustomer, True ) )
	End Using
	' do not call Complete, as we want to rollback the transaction and see if the rollback indeed succeeds.
	' as the TransactionScope goes out of scope, the on-going transaction is rolled back.
End Using
' at this point the transaction of the previous using block is rolled back.
' let the DTC and the system.transactions threads deal with the objects. 
' this sleep is only needed because we're going to access the data directly after the rollback. In normal code, 
' this sleep isn't necessary.
Thread.Sleep( 1000 )

' test if the data is still there. Shouldn't be as the transaction has been rolled back. 
Using adapter As New DataAccessAdapter()
	Dim fetchedCustomer As New CustomerEntity( NewCustomer.CustomerId )
	Assert.IsFalse( adapter.FetchEntity( fetchedCustomer ) )
	Dim fetchedAddress = New AddressEntity( NewAddress.AddressId )
	Assert.IsFalse( adapter.FetchEntity( fetchedAddress ) )
	Assert.AreEqual( 0, NewAddress.AddressId )
End Using


LLBLGen Pro v2.6 documentation. ©2002-2008 Solutions Design