Generated code - Transactions, SelfServicing

Preface

LLBLGen already had a strong transaction support and in LLBLGen Pro this tradition is continued. The generated framework supports both native database transactions and COM+ transactions. Both types are described below as well as how you can include transactional behaviour in your own code.

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 and that transaction object can be used to execute all database statements in that transaction if that connection is used. LLBLGen Pro's native database transactions and also the COM+ transactions work the same for you: you create an instance of the transaction object with the type you want (COM+ or normal) and add the objects that should participate (use) that transaction to that transaction object. As of that moment the actions you perform on those objects are executed in the transaction of that transaction object.

An example will help illustrate the usage of the Transaction object. Let's add a new order with an order row to the persistent storage for the customer "CHOPS". Because we'll add two entities, we will use a transaction to make sure that when the second save fails, the first is rolled back.

note Note:
The example below is just to show you how to use the Transaction object as it doesn't use the recursive save functionality build into the code. As recursive saves already use an ADO.NET transaction internally, the example would have been much smaller without the external transaction. To illustrate this, the same example is also listed using recursive saves. It also doesn't use the FK-PK synchronization functionality which synchronizes Foreign key fields with their Primary key field's value, after a save, for example when newOrderRow is added to newOrder.OrderDetails, which will sync the new PK value of newOrder.OrderID with newOrderRow.OrderID once newOrder has been saved. See the recursive save example below for an example of that.

First, let's see the code: (it assumes the entity data for "CHOPS" is already loaded in the object customer. The data is rather bogus, it's for illustration purposes only).

// [C#]
// Create the transaction object, pass the isolation level and give it a name
Transaction transactionManager = new Transaction(IsolationLevel.ReadCommitted, "Test");

// create a new order and then 2 new order rows. 
try
{
	// create new order entity. Use data from the object 'customer'
	OrderEntity newOrder = new OrderEntity();
	
	// set the customer reference, which will sync FK-PK values.
	// (newOrder.CustomerID = customer.CustomerID)
	newOrder.Customer = customer;
	
	newOrder.EmployeeID = 1;
	newOrder.Freight = 10;
	newOrder.OrderDate = DateTime.Now.AddDays(-3.0);
	newOrder.RequiredDate = DateTime.Now.AddDays(3.0);
	newOrder.ShipAddress = customer.Address;
	newOrder.ShipCity = customer.City;
	newOrder.ShipCountry = customer.Country;
	newOrder.ShipName = "The Bounty";
	newOrder.ShippedDate = DateTime.Now;
	newOrder.ShipRegion = customer.Region;
	newOrder.ShipVia = 1;
	newOrder.ShipPostalCode = customer.PostalCode;

	// add this new order to the transaction so actions will run inside the transaction
	transactionManager.Add(newOrder);

	// save the new order. When this fails, will throw exception which will terminate transaction.
	newOrder.Save();

	// Create new order row.
	OrderDetailsEntity newOrderRow = new OrderDetailsEntity();
	newOrderRow.OrderID = newOrder.OrderID;	// will refetch order from persistent storage.
	newOrderRow.Discount = 0;
	newOrderRow.ProductID = 10;
	newOrderRow.Quantity = 200;
	newOrderRow.UnitPrice = 31;

	// add this new orderrow to the transaction
	transactionManager.Add(newOrderRow);

	// save the new orderrow. When this fails, will throw exception which will terminate transaction.
	newOrderRow.Save();

	// done, commit the transaction
	transactionManager.Commit();
}
catch(Exception)
{
	// abort, roll back the transaction
	transactionManager.Rollback();
	throw;
}
finally
{
	// clean up. Necessary action.
	transactionManager.Dispose();
}
' [VB.NET]
' Create the transaction object, pass the isolation level and give it a name
Dim transactionManager As new Transaction(IsolationLevel.ReadCommitted, "Test")

' create a new order and then 2 new order rows. 
Try
	' create new order entity. Use data from the object 'customer'.
	Dim newOrder As New OrderEntity()
	
	' set the customer reference, which will sync FK-PK values 
	' (newOrder.CustomerID = customer.CustomerID)
	newOrder.Customer = customer
	
	newOrder.EmployeeID = 1
	newOrder.Freight = 10
	newOrder.OrderDate = DateTime.Now.AddDays(-3.0)
	newOrder.RequiredDate = DateTime.Now.AddDays(3.0)
	newOrder.ShipAddress = customer.Address
	newOrder.ShipCity = customer.City
	newOrder.ShipCountry = customer.Country
	newOrder.ShipName = "The Bounty"
	newOrder.ShippedDate = DateTime.Now
	newOrder.ShipRegion = customer.Region
	newOrder.ShipVia = 1
	newOrder.ShipPostalCode = customer.PostalCode

	' add this new order to the transaction so actions will run inside the transaction
	transactionManager.Add(newOrder)

	' save the new order. When this fails, will throw exception which will terminate transaction.
	newOrder.Save()

	' Create new order row.
	Dim newOrderRow As New OrderDetailsEntity()
	newOrderRow.OrderID = newOrder.OrderID	' will refetch order from persistent storage.
	newOrderRow.Discount = 0
	newOrderRow.ProductID = 10
	newOrderRow.Quantity = 200
	newOrderRow.UnitPrice = 31

	' add this new orderrow to the transaction
	transactionManager.Add(newOrderRow)

	' save the new orderrow. When this fails, will throw exception which will terminate transaction.
	newOrderRow.Save()

	' done, commit the transaction
	transactionManager.Commit()
Catch
	' abort, roll back the transaction
	transactionManager.Rollback()
	Throw
Finally
	' clean up. Necessary action.
	transactionManager.Dispose()
End Try

First a transaction object is created. As soon as you instantiate the object, a database connection is open and usable. This is also the reason why you have to include a finally clause and have to call Dispose() when the transaction is no longer needed. If you want an entity or entity collection object (and all its objects inside it) to participate in a transaction, add it to the particular transaction object with the Add() method. You can only add objects that can execute data modification statements like an entity object or an entity collection object, however you can't add a typed list or typed view since these objects can only read data and for reading data, transactions are not interesting.

When you add an entity or entity collection object to a transaction object, from that moment on its logic will use the connection of the transaction object to work with the database, which automatically makes sure the database statements executed will be run inside the transaction itself. The transaction object will take care of every other overhead like notifying objects when a transaction has been finished or aborted. If a transaction is aborted (Rollback() is called) or Committed (Commit() is called), all objects participating in the transaction are automatically removed from the transaction so you don't have to do that yourself.

It's best practise to embed the usage of the transaction in a try/catch/finally statement as it is done in the example above, so you can make sure when something fails during the usage of the transaction: everything is rolled back or at the end and everything is committed correctly.

Using recursive save functionality
The same functionality, but now implemented using the recursive save functionality build into the generated code. As you can see, no external transaction is used, because the code starts a new transaction internally. You can always add the entities to an existing transaction, which then makes the save actions take place inside that transaction. Recursive saves always create a normal ADO.NET transaction with isolation level ReadComitted. If you want to use a COM+ transaction with recursive saves, you have to start one by instantiating a TransactionComPlus object and then by adding the entity you want to save to that TransactionComPLus object.

// [C#]
// create a new order and then 2 new order rows. 
// create new order entity. Use data from the object 'customer'
OrderEntity newOrder = new OrderEntity();

// set the customer reference, which will sync FK-PK values.
// (newOrder.CustomerID = customer.CustomerID). You also could have said:
// newOrder.CustomerID = customer.CustomerID;
// or
// newOrder.CustomerID = _someVariable;
newOrder.Customer = customer;

newOrder.EmployeeID = 1;
newOrder.Freight = 10;
newOrder.OrderDate = DateTime.Now.AddDays(-3.0);
newOrder.RequiredDate = DateTime.Now.AddDays(3.0);
newOrder.ShipAddress = customer.Address;
newOrder.ShipCity = customer.City;
newOrder.ShipCountry = customer.Country;
newOrder.ShipName = "The Bounty";
newOrder.ShippedDate = DateTime.Now;
newOrder.ShipRegion = customer.Region;
newOrder.ShipVia = 1;
newOrder.ShipPostalCode = customer.PostalCode;

// Create new order row.
OrderDetailsEntity newOrderRow = new OrderDetailsEntity();
newOrderRow.Discount = 0;
newOrderRow.ProductID = 10;
newOrderRow.Quantity = 200;
newOrderRow.UnitPrice = 31;
// make sure the OrderID fields are synchronized when
// newOrder is saved.
newOrder.OrderDetails.Add(newOrderRow);

// save the new order, recursively. This will first save customer
// if that's changed, then newOrder, then sync newOrder.OrderID with newOrderRow.OrderID
// and then save newOrderRow. The complete Save action is done inside an ADO.NET transaction.
newOrder.Save(true);
' [VB.NET]
' create a new order and then 2 new order rows. 
' create new order entity. Use data from the object 'customer'
Dim newOrder As New OrderEntity()
' set the customer reference, which will sync FK-PK values.
' (newOrder.CustomerID = customer.CustomerID). You also could have said:
' newOrder.CustomerID = customer.CustomerID
' or
' newOrder.CustomerID = _someVariable
newOrder.Customer = customer
newOrder.EmployeeID = 1
newOrder.Freight = 10
newOrder.OrderDate = DateTime.Now.AddDays(-3.0)
newOrder.RequiredDate = DateTime.Now.AddDays(3.0)
newOrder.ShipAddress = customer.Address
newOrder.ShipCity = customer.City
newOrder.ShipCountry = customer.Country
newOrder.ShipName = "The Bounty"
newOrder.ShippedDate = DateTime.Now
newOrder.ShipRegion = customer.Region
newOrder.ShipVia = 1
newOrder.ShipPostalCode = customer.PostalCode

' Create new order row.
Dim newOrderRow As New OrderDetailsEntity()
newOrderRow.Discount = 0
newOrderRow.ProductID = 10
newOrderRow.Quantity = 200
newOrderRow.UnitPrice = 31
' make sure the OrderID fields are synchronized when
' newOrder is saved.
newOrder.OrderDetails.Add(newOrderRow)

' save the new order, recursively. This will first save customer
' if that's changed, then newOrder, then sync newOrder.OrderID with newOrderRow.OrderID
' and then save newOrderRow. The complete Save action is done inside an ADO.NET transaction.
newOrder.Save(True)

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#
' Create the transaction object, pass the isolation level and give it a name
Transaction transactionManager = new Transaction(IsolationLevel.ReadCommitted, "SavepointRollback");

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

	transactionManager.Add(newAddress);
	
	// save it.
	newAddress.Save();

	// save the transaction
	transactionManager.Save("SavepointAddress");

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

		// roll back to savepoint.
		transactionManager.Rollback("SavepointAddress");
	}
	
	// commit the transaction. If the customer save failed, 
	// only address is saved, otherwise both.
	transactionManager.Commit();
}
catch
{
	// fatal error, roll back everything
	transactionManager.Rollback();
	throw;
}
' VB.NET
' Create the transaction object, pass the isolation level and give it a name
Dim transactionManager As new Transaction(IsolationLevel.ReadCommitted, "SavepointRollback")

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

	transactionManager.Add(newAddress)
	
	' save it.
	newAddress.Save()

	' save the transaction
	transactionManager.Save("SavepointAddress")

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

		' roll back to savepoint.
		transactionManager.Rollback("SavepointAddress")
	End Try
	
	' commit the transaction. If the customer save failed, 
	' only address is saved, otherwise both.
	transactionManager.Commit()
Catch
	// fatal error, roll back everything
	transactionManager.Rollback()
	Throw
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 as well through a special transaction class TransactionComPlus, instead of the normal class Transaction. Because COM+ is implemented in .NET using Enterprise Services, the class using the TransactionComPlus object has to derive from ServicedComponent. This way, transactions started outside the class using the TransactionComPlus class can flow through to the TransactionComPlus class. Also, you have to reference the System.EnterpriseServices namespace in your code. Below is a short example how a COM+ transaction flows through to the 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.

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 the code will cause some overhead on your system: it has a context inside COM+, which is a native windows service. Most of the time you can fulfill your transactional requirements using native database transactions with the normal Transaction class in the generated code, as illustrated in the previous section.


// [C#]
[Transaction(TransactionOption.Required)]
public class TestClass : ServicedComponent
{
	[AutoComplete]
	public void TestComPlus()
	{
		CustomerEntity customer = new CustomerEntity("CHOPS");
		OrderEntity order = new OrderEntity(10254);

		TransactionComPlus comPlusTransaction = new TransactionComPlus();
		comPlusTransaction.Add(customer);
		comPlusTransaction.Add(order);

		try
		{
			customer.Country="Brazil";
			customer.Save();

			order.ShipName="The WaveKiller";
			order.Save();

			comPlusTransaction.Commit();
		}
		catch
		{
			// abort
			comPlusTransaction.Rollback();

			// bubble exception OR call setAbort on current context.
			throw;
		}
		finally
		{
			comPlusTransaction.Dispose();
		}
	}
}
' [VB.NET]
<Transaction(TransactionOption.Required)> _
Public Class TestClass 
	Inherits ServicedComponent

	<AutoComplete> _
	Public Sub TestComPlus()
		Dim customer As New CustomerEntity("CHOPS")
		Dim order As New OrderEntity(10254)

		Dim comPlusTransaction As New TransactionComPlus()
		comPlusTransaction.Add(customer)
		comPlusTransaction.Add(order)

		Try
			customer.Country="Brazil"
			customer.Save()

			order.ShipName="The WaveKiller"
			order.Save()

			comPlusTransaction.Commit()
		Catch
			' abort
			comPlusTransaction.Rollback()

			' bubble exception OR call setAbort on current context.
			Throw
		Finally
			comPlusTransaction.Dispose()
		End Try
	End Sub
End Class

The TestComPlus method will now anticipate in an existing COM+ transaction and will, if the method succeeds, automatically commit the transaction, provided the method doesn't throw an exception. If it does, the transaction is considered aborted and will roll back.

.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.
}

It's recommended to read the System.Transactions documentation in the MSDN documentation archive for further background info on this new namespace and its classes.

An LLBLGen Pro Transaction 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 Transaction object (if any) that it can commit/rollback the transaction. That call will then notify the enlisted entities of the outcome of the transaction.
Example
Below is an example which shows the usage of a TransactionScope in combination of a Transaction 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() )
{
	// start a new LLBLGen Pro transaction
	using( Transaction trans = new Transaction(System.Data.IsolationLevel.ReadCommitted, "Test") )
	{
		newCustomer.VisitingAddress = newAddress;
		newCustomer.BillingAddress = newAddress;
		// add the entities to save to the LLBLGen Pro transaction
		trans.Add( newCustomer );
		trans.Add( newAddress );
		// save both entities.
		Assert.IsTrue(newCustomer.Save( 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. 
CustomerEntity fetchedCustomer = new CustomerEntity( customerId );
Assert.AreEqual( EntityState.New, fetchedCustomer.Fields.State );
AddressEntity fetchedAddress = new AddressEntity( addressId);
Assert.AreEqual( EntityState.New, fetchedAddress.Fields.State );
 '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() 
	' start a New LLBLGen Pro transaction
	Using trans As New Transaction(System.Data.IsolationLevel.ReadCommitted, "Test") 
		NewCustomer.VisitingAddress = NewAddress
		NewCustomer.BillingAddress = NewAddress
		' add the entities to save to the LLBLGen Pro transaction
		trans.Add( NewCustomer )
		trans.Add( NewAddress )
		' save both entities.
		Assert.IsTrue(NewCustomer.Save( 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. 
Dim fetchedCustomer As New CustomerEntity( customerId )
Assert.AreEqual( EntityState.New, fetchedCustomer.Fields.State )
Dim fetchedAddress As New AddressEntity( addressId)
Assert.AreEqual( EntityState.New, fetchedAddress.Fields.State )


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