Entity Framework transaction scope examples
Transactions as a core building block of entity framework. The DbContext object which we heavily use for interacting with the database uses transactions internally without you having to do anything extra. In case you need to manually provide transaction support, here is how you can do it.
In this post, I cover three cases in which transaction scope is being used to show rollback when an error occurs during an update of multiple entities:
- when you have multiple save calls to the context;
- when you have single save with multiple object; and
- transactions across multiple contexts.
Our model is a simplified version of a “Product” object and it’s definition look like this:
using System; using System.ComponentModel.DataAnnotations; namespace EFTransactionsDemo.Model { public class Product { public int Id { get; set; } [Required] public string Name { get; set; } [Required] public DateTime DateAdded { get; set; } } }
Our context definition looks like:
using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; namespace EFTransactionsDemo.Model { public class EFTDbContext : DbContext { public DbSet<Product> Products { get; set; } public EFTDbContext() : base("EFTransactionsDemo") { // disable proxy creation this.Configuration.ProxyCreationEnabled = false; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Tell Code First to ignore PluralizingTableName convention // If you keep this convention then the generated tables will have pluralized names. modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); } } }
The main method (this is demo code; in production environments, please make sure to use proper programming paradigms, do error handling, parameter checks, etc.), calls three functions that demonstrate the use of transaction scope for the three scenarios I listed above.
using System; using System.Linq; using System.Transactions; using EFTransactionsDemo.Model; namespace EFTransactionsDemo { class Program { static void Main(string[] args) { // setup CreateDatabase(); // transaction demo - 1 // wrap multiple save calls within one transaction // and roll back on a bad one TestMultipleSaveCalls(); // transaction demo - 2 // wrap multiple adds and one save within one transaction // and roll back on a bad one TestMultipleAddWithOneSaveCall(); // transaction demo - 3 // multiple contexts in same transaction TestMultipleContexts(); } private static void TestMultipleSaveCalls() { // good product Product goodProduct = new Product() { Name = "My awesome book", DateAdded = DateTime.UtcNow }; // invalid product Product invalidProd = new Product() { Name = "My awesome book part 2" // note - no date added specified }; // define our transaction scope var scope = new TransactionScope( // a new transaction will always be created TransactionScopeOption.RequiresNew, // we will allow volatile data to be read during transaction new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted } ); try { // use the scope we just defined using (scope) { // create a new db context using (var ctx = new EFTDbContext()) { // add the product ctx.Products.Add(goodProduct); // save ctx.SaveChanges(); // add the invalid product ctx.Products.Add(invalidProd); // save ctx.SaveChanges(); } // everything good; complete scope.Complete(); } } catch { } // verify that we actually rolled back using (var ctx = new EFTDbContext()) { Console.WriteLine(ctx.Products.Count()); } } private static void TestMultipleAddWithOneSaveCall() { // good product Product goodProduct = new Product() { Name = "My awesome book", DateAdded = DateTime.UtcNow }; // invalid product Product invalidProd = new Product() { Name = "My awesome book part 2" // note - no date added specified }; // define our transaction scope var scope = new TransactionScope( // a new transaction will always be created TransactionScopeOption.RequiresNew, // we will allow volatile data to be read during transaction new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted } ); try { // use the scope we just defined using (scope) { // create a new db context using (var ctx = new EFTDbContext()) { // add the product ctx.Products.Add(goodProduct); // add the invalid product ctx.Products.Add(invalidProd); // save ctx.SaveChanges(); } // everything good; complete scope.Complete(); } } catch { } // verify that we actually rolled back using (var ctx = new EFTDbContext()) { Console.WriteLine(ctx.Products.Count()); } } private static void TestMultipleContexts() { // good product Product goodProduct = new Product() { Name = "My awesome book", DateAdded = DateTime.UtcNow }; // invalid product Product invalidProd = new Product() { Name = "My awesome book part 2" // note - no date added specified }; // define our transaction scope var scope = new TransactionScope( // a new transaction will always be created TransactionScopeOption.RequiresNew, // we will allow volatile data to be read during transaction new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted } ); try { // use the scope we just defined using (scope) { // create a new db context var firstCtx = new EFTDbContext(); // create a second context var secondCtx = new EFTDbContext(); // add the product to first context firstCtx.Products.Add(goodProduct); // save firstCtx.SaveChanges(); // add the invalid product to second context secondCtx.Products.Add(invalidProd); // save secondCtx.SaveChanges(); // everything good; complete scope.Complete(); } } catch { } // verify that we actually rolled back using (var ctx = new EFTDbContext()) { Console.WriteLine(ctx.Products.Count()); } } private static void CreateDatabase() { using (var ctx = new EFTDbContext()) { ctx.Database.CreateIfNotExists(); } } } }
As always, please do leave your thoughts and comments. It is the user feedback that drives the blog better.