Handling multiple context transactions

In this recipe, we are going to be using transactions to tightly control the communication of multiple contexts to a database.

Getting ready

We will be using the NuGet Package Manager to install the Entity Framework 4.1 assemblies.

The package installer can be found at http://nuget.org.

We will also be using a database for connecting data and updating it.

Open the Improving Multiple Context Transactions solution in the included sourcecode examples.

How to do it...

  1. We start by adding a new unit test named TransactionTests to the test project. We make a test that connects to the database and adds an object within several transaction usages by using the following code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Validation;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Transactions;
    using BusinessLogic;
    using DataAccess;
    using DataAccess.Database;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Test.Properties;
    using System.Data.Entity;
    
    namespace Test
    {
      [TestClass]
      public class TransactionTests
      {
    
        [TestMethod]
        public void ShouldRollBackMultipleContextSaveCalls()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
          var context2 = new BlogContext(Settings.Default.BlogConnection);
    
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Test",
            Title = "Testing"
          };
    
          var badBlog = new Blog()
          {
            Creationdate = DateTime.Now,
            Title = null,
            ShortDescription = null,
            Rating = 1.0
          };
    
          //Act
          var scope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions()
          {
            IsolationLevel = IsolationLevel.ReadUncommitted
          });
    
          try
          {
            using (scope)
            {
              context.Set<Blog>().Add(blog);
              context.SaveChanges();
    
              context2.Set<Blog>().Add(badBlog);
              context2.SaveChanges();
            }
    
          }
          catch (Exception)
          {
    
          }
    
          //Assert
          Assert.AreEqual(0, context.Find<Blog>().Count(x => x.Title == "Test"));
        }
    
        [TestMethod]
        public void ShouldAllowImplicitTransactionsForRollback()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          var context2 = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
    
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Test",
            Title = "Testing"
          };
    
          //Act
          using (var scope = new TransactionScope(TransactionScopeOption.Required,new TransactionOptions(){IsolationLevel = IsolationLevel.ReadCommitted}))
          {
            context.Set<Blog>().Add(blog);
            context2.Set<Blog>().Add(blog);
            //Not calling scope.Complete() here causes a rollback.   
          }
    
          //Assert
          Assert.AreEqual(0,context.Find<Blog>().Count(x=>x.Title == "Test"));
          Assert.AreEqual(0, context2.Find<Blog>().Count(x => x.Title == "Test"));
        }
      }
    
    }
  2. Add an initializer to the DataAccess project in the Database folder with the following code to set up the data:
    using System;
    using System.Data.Entity;
    using BusinessLogic;
    
    namespace DataAccess.Database
    {
    
      public class Initializer : DropCreateDatabaseAlways<BlogContext>
      {
        public Initializer()
        {
    
        }
        protected override void Seed(BlogContext context)
        {
          context.Set<Blog>().Add(new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Testing",
            Title = "Test Blog"
          });
          context.SaveChanges();
        }
      }
    }
  3. In the BusinessLogic project, add a new C# class named Blog with the following code:
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.Text.RegularExpressions;
    
    namespace BusinessLogic
    {
      public class Blog
      {
        public int Id { get; set; }
        public DateTime Creationdate { get; set; }
        public string ShortDescription { get; set; }
        public string Title { get; set; }
        public double Rating { get; set; }
      }
    }
  4. Add a Mapping folder to the DataAccess project, and add a BlogMapping class to the folder with the following code:
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.ModelConfiguration;
    using BusinessLogic;
    
    namespace DataAccess.Mappings
    {
      public class BlogMapping : EntityTypeConfiguration<Blog>
      {
        public BlogMapping()
        {
          this.ToTable("Blogs");
          this.HasKey(x => x.Id);
          this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
            .HasColumnName("BlogId");
    
          this.Property(x => x.Title).IsRequired().HasMaxLength(250);
          this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired();
          this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description");
        }
    
      }
    }
  5. Modify the BlogContext class to contain the new mappings and a DbSet property for Blog with the following code:
    using System;
    using System.Data.Entity;
    using System.Linq;
    using BusinessLogic;
    using DataAccess.Mappings;
    
    namespace DataAccess
    {
      public class BlogContext : DbContext, IUnitOfWork
      {
        public BlogContext(string connectionString) : base(connectionString)
        {
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
          modelBuilder.Configurations.Add(new BlogMapping());
          base.OnModelCreating(modelBuilder);
        }
    
        public IQueryable<T> Find<T>() where T : class
        {
          return this.Set<T>();
        }
    
        public void Refresh()
        {
          this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload());
        }
    
        public void Commit()
        {
          this.SaveChanges();
        }
      }
    }
  6. Run our test, and see how it works.

How it works...

We start our solution, as always, by creating a set of tests that communicate our intent, and allow us to verify our results. In this case, we are testing that two separate connections to the databases can be managed by a single transaction.

The system that allows this is compatible with Entity Framework, because of how Entity Framework was built. This structure of ADO.NET connections and generated SQL commands allows us to use the Distributed Transaction Coordinator (DTC). This is a feature of modern versions of Windows, and therefore is restricted to a Windows platform.

This communication not only sets up a transaction on both the database communications, but also wraps the resources (in this case, our DbContexts) in a large OS-level transaction. The execution of these statements depends on the program completing the transaction scope. This gives us fine-grain control over these multi-context transactions.

There's more...

When dealing with distributed transactions, we want to leverage them in a way that makes use of the ACID principles.

Atomic

The execution of any transaction should either have the full-intended effect or no effect at all. The results should be either complete (commit), or nothing should happen (abort).

Consistent

Any transaction is a transition of state in an application, and therefore should preserve a consistent version of the application. For example, when updating a many-to-many relationship, both the foreign key and the reference table relationship should be updated.

Isolation

Each transaction should be isolated from all other incomplete transactions. Due to the transactions being in the state of transition, they are not consistent, and therefore should be removed from affecting the transaction that is currently executing.

Durability

System failures should not cause a committed transaction that fails to persist its effects. If we rename something, but the SQL server crashes in the middle of the commit operation, on recovery the transaction should still be fully committed. (This normally involves another call to the database to execute the commit operation again, but it is handled by the DTC.)

..................Content has been hidden....................

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