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.
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.
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")); } } }
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(); } } }
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; } } }
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"); } } }
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(); } } }
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 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.
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.
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.
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.