Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use the same transaction in different methods with Entity Framework Core

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.

like image 224
AdrienTorris Avatar asked Nov 15 '16 14:11

AdrienTorris


2 Answers

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>();
like image 165
regnauld Avatar answered Sep 29 '22 10:09

regnauld


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();
        }
    }
like image 39
AdrienTorris Avatar answered Sep 29 '22 12:09

AdrienTorris