Reliable integration with other systems is a common business requirement. When these systems report error conditions, it's necessary to roll back not only the local database work, but perhaps the work of multiple transactional resources. In this recipe, I'll show you how to use Microsoft's TransactionScope and NHibernate to achieve this goal.
Create a new console application project.
Add references to the Eg.Core
project in Chapter 1, NHibernate.dll
, and NHibernate.ByteCode.Castle.dll
.
Get the console application ready by following the Configuring NHibernate with App.config and Configuring log4net recipes in Chapter 2.
System.Transaction
.IReceiveProductUpdates
with the following three methods:void Add(Product product); void Update(Product product); void Remove(Product product);
WarehouseFacade
with this code:public class WarehouseFacade : IReceiveProductUpdates { public void Add(Product product) { Console.WriteLine("Adding {0} to warehouse system.", product.Name); } public void Update(Product product) { Console.WriteLine("Updating {0} in warehouse system.", product.Name); } public void Remove(Product product) { Console.WriteLine("Removing {0} from warehouse system.", product.Name); var message = string.Format( "Warehouse still has inventory of {0}.", product.Name); throw new ApplicationException(message); } }
ProductCatalog
with this code:public class ProductCatalog : IReceiveProductUpdates { private readonly ISessionFactory _sessionFactory; public ProductCatalog(ISessionFactory sessionFactory) { _sessionFactory = sessionFactory; } public void Add(Product product) { Console.WriteLine("Adding {0} to product catalog.", product.Name); using (var session = _sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { session.Save(product); tx.Commit(); } } public void Update(Product product) { Console.WriteLine("Updating {0} in product catalog.", product.Name); using (var session = _sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { session.Update(product); tx.Commit(); } } public void Remove(Product product) { Console.WriteLine("Removing {0} from product catalog.", product.Name); using (var session = _sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { session.Delete(product); tx.Commit(); } } }
Program
class with the following code:class Program { static void Main(string[] args) { var nhConfig = new Configuration() .Configure(); var sessionFactory = nhConfig .BuildSessionFactory(); var catalog = new ProductCatalog(sessionFactory); var warehouse = new WarehouseFacade(); var p = new Program(catalog, warehouse); var sprockets = new Product() { Name = "Sprockets", Description = "12 pack, metal", UnitPrice = 14.99M }; p.AddProduct(sprockets); sprockets.UnitPrice = 9.99M; p.UpdateProduct(sprockets); p.RemoveProduct(sprockets); Console.WriteLine("Press any key."); Console.ReadKey(); } private readonly IReceiveProductUpdates[] _services; public Program(params IReceiveProductUpdates[] services) { _services = services; } private void AddProduct(Product newProduct) { Console.WriteLine("Adding {0}.", newProduct.Name); try { using (var scope = new TransactionScope()) { foreach (var service in _services) service.Add(newProduct); scope.Complete(); } } catch (Exception ex) { Console.WriteLine("Product could not be added."); Console.WriteLine(ex.Message); } } private void UpdateProduct(Product changedProduct) { Console.WriteLine("Updating {0}.", changedProduct.Name); try { using (var scope = new TransactionScope()) { foreach (var service in _services) service.Update(changedProduct); scope.Complete(); } } catch (Exception ex) { Console.WriteLine("Product could not be updated."); Console.WriteLine(ex.Message); } } private void RemoveProduct(Product oldProduct) { Console.WriteLine("Removing {0}.", oldProduct.Name); try { using (var scope = new TransactionScope()) { foreach (var service in _services) service.Remove(oldProduct); scope.Complete(); } } catch (Exception ex) { Console.WriteLine("Product could not be removed."); Console.WriteLine(ex.Message); } } }
NHCookbook
database. You should find a Product
row for Sprockets with a unit price of $9.99.In this recipe, we work with two services that receive product updates. The first, a product catalog, uses NHibernate to store product data. The second, a small facade, is not as well-defined. It could use a number of different technologies to integrate our application with the larger warehouse system it represents.
Our services allow us to add, update, and remove products in these two systems. By wrapping these changes in a TransactionScope
, we gain the ability to roll back the product catalog changes if the warehouse system fails, maintaining a consistent state.
Remember that NHibernate requires an NHibernate transaction when interacting with the database. TransactionScope
is not a substitute. As illustrated in the next image, the TransactionScope
should completely surround both the session and NHibernate transaction. The call to TransactionScope.Complete()
should occur after the session has been disposed. Any other order will most likely lead to nasty, production crashing bugs like connection leaks.
When we attempt to remove a product, our WarehouseFacade
throws an exception, and things get a little strange. We committed the NHibernate transaction, so why didn't our delete happen? It did, but it was rolled back by the TransactionScope
. When we started our NHibernate transaction, NHibernate detected the ambient transaction created by the TransactionScope
and enlisted. The underlying connection and database transaction were held until the TransactionScope
committed, or in this case, rolled back.