Use the same transaction in different methods with Entity Framework Core

AdrienTorris picture AdrienTorris · Nov 15, 2016 · Viewed 10.4k times · Source

EDIT (02/03/2018) : Since Entity Framework Core 2.1, EF Core implements transactions, cross-context transactions, ambient transactions and transactions scopes so this question is now out of date.

This is the official documentation about the transactions in EF Core : https://docs.microsoft.com/en-us/ef/core/saving/transactions.


How can I use the same transaction in differents methods ? The objective is to can commit or rollback all the modification if an error occurred.

I'm using Entity Framework Core version 1.1.0-preview1-final, and SQL Server 2014.

For example, I have an Entity Framework database context :

public class ApplicationDatabaseContext : DbContext
    {
        public ApplicationDatabaseContext(DbContextOptions<ApplicationDatabaseContext> options)
           : base(options)
        { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TransactionLog1>(entity =>
            {
                entity.ToTable("TRANSACTION_LOG_1");

                entity.Property(e => e.CreationDate)
                    .HasColumnType("datetime")
                    .HasDefaultValueSql("getdate()");
            });

            modelBuilder.Entity<TransactionLog2>(entity =>
            {
                entity.ToTable("TRANSACTION_LOG_2");

                entity.Property(e => e.CreationDate)
                    .HasColumnType("datetime")
                    .HasDefaultValueSql("getdate()");
            });
        }

        public virtual DbSet<TransactionLog1> TransactionLog1 { get; set; }
        public virtual DbSet<TransactionLog2> TransactionLog2 { get; set; }
    }

And I have two classes to deal with data, the both are using the same context :

public interface IRepository1
{
    void Create(Guid key);
}

public sealed class Repository1 : IRepository1
{
    private readonly ApplicationDatabaseContext _dbContext;

    public Repository1(ApplicationDatabaseContext dbcontext)
    {
        _dbContext = dbcontext;
    }

    public void Create(Guid key)
    {
        using (_dbContext.Database.BeginTransaction())
        {
            try
            {
                _dbContext.TransactionLog1.Add(new TransactionLog1 { Key = key });
                _dbContext.SaveChanges();

                _dbContext.Database.CommitTransaction();
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

public interface IRepository2
{
    void Create(Guid key);
}

public sealed class Repository2 : IRepository2
{
    private readonly ApplicationDatabaseContext _dbContext;

    public Repository2(ApplicationDatabaseContext dbcontext)
    {
        _dbContext = dbcontext;
    }

    public void Create(Guid key)
    {
        using (_dbContext.Database.BeginTransaction())
        {
            try
            {
                _dbContext.TransactionLog2.Add(new TransactionLog2 { Key = key });
                _dbContext.SaveChanges();

                _dbContext.Database.CommitTransaction();
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

In my business logic, I have a service and I would like to call the method void Create(Guid key) on my first repository, then the same method from my second repository and commit only if the both occurred without error (if any error occurred in the secon method, I would like to rollback the commit done in the first method).

How can I do that ? What is the best practice with Entity Framework Core and transactions ?

I tried several things, like this, but it never works (with this method I have the error ) :

Warning as error exception for warning 'RelationalEventId.AmbientTransactionWarning': An ambient transaction has been detected. Entity Framework Core does not support ambient transactions.

public sealed class Service3 : IService3
{
        private readonly IRepository1 _repo1;
        private readonly IRepository2 _repo2;

        public Service3(IRepository1 repo1, IRepository2 repo2)
        {
            _repo1 = repo1;
            _repo2 = repo2;
        }

        public void Create(Guid key)
        {
            using (TransactionScope scope = new TransactionScope())
            {
                try
                {
                    _repo1.Create(key);
                    _repo2.Create(key);

                    scope.Complete();
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }
}

I read the documentation, espacially this page (https://docs.microsoft.com/en-us/ef/core/saving/transactions) but I don't have the method UseTransaction on Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.

Answer

AdrienTorris picture AdrienTorris · Nov 16, 2016

EDIT (02/03/2018) : Since Entity Framework Core 2.1, you can use transactions, cross-context transactions, ambient transactions and transactions scopes so you don't have to implement a work-around.

This is the official documentation : https://docs.microsoft.com/en-us/ef/core/saving/transactions.


I finally found a solution waiting the next release of Entity Framework Core who will enable to use transaction scopes and ambient transactions.

As the db transaction is related to a database context, and the database context is the same in all my data access classes (thank's dependency injection), when I start a transaction in a process, it will be shared by others data access classes in the same process until the transaction will be disposed (I had to upgrade my Entity Framework Core to 1.1.0-preview1-final to have disposable transactions).

Concretely, I had a class to deal with transactions :

public interface ITransactionDealerRepository
{
    void BeginTransaction();

    void CommitTransaction();

    void RollbackTransaction();

    void DisposeTransaction();
}

public sealed class TransactionDealerRepository : BaseEntityFrameworkRepository, ITransactionDealerRepository
{
    public TransactionDealerRepository(MyDBContext dbContext)
       : base(dbContext)
    { }

    public void BeginTransaction()
    {
        _dbContext.Database.BeginTransaction();
    }

    public void CommitTransaction()
    {
        _dbContext.Database.CommitTransaction();
    }

    public void RollbackTransaction()
    {
        _dbContext.Database.RollbackTransaction();
    }

    public void DisposeTransaction()
    {
        _dbContext.Database.CurrentTransaction.Dispose();
    }
}

And I use this class like this in my services :

    public void Create(Guid key)
    {
        _transactionProvider.BeginTransaction();

        try
        {
            _repo1.Create(key);
            _repo2.Create(key);

            _transactionProvider.CommitTransaction();
        }
        catch (Exception)
        {
            _transactionProvider.RollbackTransaction();
            throw;
        }
        finally
        {
            _transactionProvider.DisposeTransaction();
        }
    }