Use the same transaction in different methods with Entity Framework Core

advertisements

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.


One possible approach is to use a middleware and put your logic for begin/commit/rollback there. For example, at the beginning of each request you begin a transaction on the underlying database connection. At the end of the request commit or rollback the transaction. Since you most probably use single context instance per request, this would solve your problem. Additionally you will extract this concern from your repository/service classes.

Here is a sample code you might you use as a startup. Haven't tested it in real scenario though:

public class TransactionPerRequestMiddleware
{
    private readonly RequestDelegate next_;

    public TransactionPerRequestMiddleware(RequestDelegate next)
    {
        next_ = next;
    }

    public async Task Invoke(HttpContext context, ApplicationDbContext dbContext)
    {
        var transaction = dbContext.Database.BeginTransaction(
            System.Data.IsolationLevel.ReadCommitted);

        await next_.Invoke(context);

        if (context.Response.StatusCode == 200)
        {
            transaction.Commit();
        }
        else
        {
            transaction.Rollback();
        }
    }
}

Then in your Startup.Configure() method:

app.UseMiddleware<TransactionPerRequestMiddleware>();