Using transaction scopes

In this recipe, we will create a transaction scope that will allow multiple save operations on a single context to be handled as a single transaction, committing or rolling back, as needed.

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 to the data and updating it.

Open the Improving Transaction Scope solution in the included source code 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 ShouldRollBackMultipleSaveContextCalls()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
    
          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();
    
              context.Set<Blog>().Add(badBlog);
              context.SaveChanges();
            }
    
          }
          catch (Exception)
          {
    
          }
    
          //Assert
          Assert.AreEqual(0, context.Find<Blog>().Count(x => x.Title == "Test"));
        }
    
        [TestMethod]
        public void ShouldRollbackMultipleObjectsOnSingleBadSave()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
    
          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
          try
          {
            var set = context.Set<Blog>();
            set.Add(blog);
            set.Add(badBlog);
            context.SaveChanges();
          }
          catch
          {
    
          }
    
          //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);
          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);
            context.SaveChanges();
            //Not calling scope.Complete() here causes a rollback.
          }
    
          //Assert
          Assert.AreEqual(0,context.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...

By creating our tests for transaction support, we accomplish two goals. First we demonstrate in a very clear code what functionality the context has and should have for verification. Second we demonstrate the usage code for a transaction scope.

This usage translates into a transaction scope in the SQL statement, so even outside our code, it is enforced to be a single transaction. Using transaction scope in this context makes for a very clear bundle of work headed for the database. The DbContext does this on a small-scale every time it saves to the database, which allows us to ignore the transaction scope most of the time. If you need everything from a single save call to be wrapped in a transaction, then you need not write any addition code as that is the default behavior of the DbContext.

We are using the standard transactions for interacting with the Entity Framework, which will also allow us to write in other data access code, if we had to, without modifying our transaction code.

There's more...

There are some key tenants that we will want to adhere to when talking about and working with transactions. These will save us from making very small mistakes with very large effects.

Ensuring read/update separation

When we are write a transaction, we want to make sure that only our updated code is covered by the transaction, our reads should not be, if at all, possible. This will allow for smaller and faster transactions. Keeping a transaction small allows us to avoid many of the locking concerns that could plague our application, otherwise.

Triggering – careful now

We have to be very careful when updating a table with a trigger in a transaction, because when we do this, the trigger is also included in the transaction scope. If the trigger fails, it will roll back the entire transaction. The transaction includes the trigger as soon as the trigger is fired.

Remembering that size matters

We have to read as few rows as possible in the transaction, and try to avoid stringing many operations together within a single transaction. This keeps our transaction scope in control. If we ignore this, then our transactions will bloat to the point of adversely effecting performance, and user experience.

See also

In this chapter:

  • Handling multiple context transactions
..................Content has been hidden....................

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