In this recipe, we are going to be using transactions to tightly control the communication of multiple contexts to a database.
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.
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")); } } }
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(); } } }
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.
When dealing with distributed transactions, we want to leverage them in a way that makes use of the ACID principles.
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
).
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.
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.
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.)