Using NHibernate with transaction scope

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.

Getting ready

Follow the Getting ready step in the Save entities to the database recipe in this chapter.

How to do it…

  1. Add a reference to System.Transaction.
  2. Add a folder named UsingTransactionScope to the project.
  3. Add a public interface named IReceiveProductUpdatesto the folder:
    using NH4CookbookHelpers.Queries.Model;
    
    namespace SessionRecipes.UsingTransactionScope
    {
      public interface IReceiveProductUpdates
      {
        void Add(Product product);
        void Update(Product product);
        void Remove(Product product);
      }
    }
  4. Add a public class named 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);
      }
    
    }
  5. Add a public class named 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();
        }
      }
    
    }
  6. Add a class named 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);
          }
        }
      }
    }
  7. Add a new class named 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);
        }
      }
    }
  8. Run the application and start the UsingTransactionScope recipe. You should see this output:
    How to do it…
  9. Check the database (in the tables tab). You should find a Product row for Sprocketswith a unit price of $9.99.

How it works…

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.

See also

  • Creating a session ASP.NET MVC action filter
  • Creating a transaction ASP.NET MVC action filter
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.45.5