8

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://learn.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://learn.microsoft.com/en-us/ef/core/saving/transactions) but I don't have the method UseTransaction on Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.

AdrienTorris
  • 9,111
  • 9
  • 34
  • 52
  • After more research I don't think it's possible with the current version of Entity Framework Core : https://github.com/dotnet/corefx/issues/12534 . It seems to be planned for the next version – AdrienTorris Nov 15 '16 at 14:57

3 Answers3

9

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>();
regnauld
  • 4,046
  • 3
  • 23
  • 22
  • Hum interesting. I finally found a solution in a different way but your solution makes sense, I'll test to see if it's better than what I finally do – AdrienTorris Nov 16 '16 at 07:57
  • I just posted an answer with the solution I found last night. I will test yours, I'll tell you if it's better than mine. Thank's anyway – AdrienTorris Nov 16 '16 at 08:07
  • once entity framework itself wrap every request as transaction, you can avoid this middleware, and avoid explicit transactions and explicit "saveChanges", and u are get the same results... am I wrong? – arielorvits Dec 28 '16 at 13:51
  • Does this not has any impact on the database performance because you wrap every request on a transaction? – Zinov Mar 01 '18 at 20:49
  • 1
    As stated in MSDN "Beginning a transaction requires that the underlying store connection is open. So calling Database.BeginTransaction() will open the connection if it is not already opened.". You should not do that per whole request time. – Mabakay Mar 10 '21 at 09:41
8

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://learn.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();
        }
    }
AdrienTorris
  • 9,111
  • 9
  • 34
  • 52
  • hello @Adrien, did you miss method savechanges()? – SheldonLou Mar 02 '17 at 15:24
  • 1
    @SheldonLou You have to call your_db_context.SaveChanges() or your_db_context.SaveChangesAsync() but this step is done in the data access layer, and not in the business logic layer. In my example, the changes are saved in the two methods : _repo1.Create(key) and _repo2.Create(key) – AdrienTorris Mar 02 '17 at 15:39
  • Good, but you've got some problems with your example code. You didn't define/inject _dbContext, for example. Also, in what way are you registering this TransactionDealerRepository in the IServiceCollection? – starmandeluxe Dec 07 '17 at 07:25
  • @AndrienTorris, when you Rollback your transaction, you CurrentTransaction on the DisposeTransaction not longer exist, so there is no necessity to leave the finally statement on the code – Zinov Mar 02 '18 at 15:06
  • Btw did you check the RollbackTransaction? it doesn't work for me – Zinov Mar 02 '18 at 15:12
  • @Zinov Yes I checked but this implementation is obsolete now, Entity Framework Core implements now the transactions and the transaction scope. This is the documentation : https://learn.microsoft.com/en-us/ef/core/saving/transactions – AdrienTorris Mar 02 '18 at 17:26
  • @AdrienTorris , Does it mean that `_dbContext.Database.CurrentTransaction.Dispose();` call is obselete now? Would that make the above approach you mentioned risky in terms of `_dbContext.Database.CurrentTransaction.Dispose()` api becoming deprecated? OR is it fine to call `Dispose()` in some specific situation like how `Dispose()` is called in the code you have given above. – Sharath Aug 10 '20 at 06:01
1

once entity framework itself wrap every request as transaction, you can avoid explicit transactions and explicit "saveChanges", and u are get all the request committed or rollbacked atomically

arielorvits
  • 5,235
  • 8
  • 36
  • 61