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, we'll show you how to use Microsoft's transaction scope and NHibernate to achieve this goal.
Follow the Getting ready step in the Save entities to the database recipe in this chapter.
System.Transaction
.UsingTransactionScope
to the project.IReceiveProductUpdates
to the folder:using NH4CookbookHelpers.Queries.Model; namespace SessionRecipes.UsingTransactionScope { public interface IReceiveProductUpdates { 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(); } } }
ProductApp
with the following code:using System; using System.Transactions; using NH4CookbookHelpers.Queries.Model; namespace SessionRecipes.UsingTransactionScope { public class ProductApp { private readonly IReceiveProductUpdates[] _services; public ProductApp(params IReceiveProductUpdates[] services) { _services = services; } public 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); } } public 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); } } public 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); } } } }
Recipe
to the folder:using NH4CookbookHelpers.Queries; using NH4CookbookHelpers.Queries.Model; using NHibernate; namespace SessionRecipes.UsingTransactionScope { public class Recipe : QueryRecipe { protected override void Run(ISessionFactory sessionFactory) { var catalog = new ProductCatalog(sessionFactory); var warehouse = new WarehouseFacade(); var p = new ProductApp(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); } } }
UsingTransactionScope
recipe. You should see this output: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 that 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 NHibernate transactions when interacting with the database. TransactionScope
is not a substitute. As illustrated in the following figure, the TransactionScope
should completely surround both the session and NHibernate transactions. The call toTransactionScope.Complete()
should occur after the session has been disposed. Any other order will most likely lead to nasty, production crashing bugs, such as 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.
3.17.154.139