Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

`DbContext.Database.BeginTransaction` can't be nested?

I'm trying the EF6-way of using transactions and noticed that the following code

using (db.Database.BeginTransaction())
{
    // something
    using (db.Database.BeginTransaction())
    {
        /// something
    }
}

throws an exception on the second begin transaction telling me that

The connection is already in a transaction and cannot participate in another
transaction. EntityClient does not support parallel transactions.

Not that I would want to do a parallel transaction, whatever that could mean on one connection. I just wanted a nested one.

So is this indeed not supported on what's going on?

like image 913
John Avatar asked Jun 28 '16 10:06

John


2 Answers

You can use the CurrentTransaction property.

using (db.Database.BeginTransaction())
{
    if(db.Database.CurrentTransaction!=null)
    {
     /// something    
    }
    else
    {
        using (db.Database.BeginTransaction())
        {
         /// the same something
        }
    }
}

The syntax is rather awkward. I often rely on an extension method to wrap the something into a transaction

    public static void WrapInTransaction(this DbContext db, Action something)
    {
        if (db.Database.CurrentTransaction != null)
            something();
        else
            using (db.Database.BeginTransaction())
            {
                something();
            }
    }
like image 84
jbl Avatar answered Oct 20 '22 02:10

jbl


Limitation: I assume, when the inner transaction failed, the outer transaction should fail too. So you don't want to rollback and do something other on the inner one.

I just got the the problem, when overriding SaveChanges, to apply some database change tracker, but don't know, if we already have a transaction. So this defines the usecase.

I created a custom transaction, which only starts a transaction, if we do not already have one. Any commit/rollback/dispose are only applied, when we had initial no transaction:

public class FallbackTransaction : IDisposable
{
    private readonly System.Data.Entity.Database _database;

    private DbContextTransaction _fallbackTransaction;

    public FallbackTransaction(System.Data.Entity.Database database)
    {
        _database = database;
    }

    public void Dispose()
    {
        _fallbackTransaction?.Dispose();
    }

    public void Begin()
    {
        if (_database.CurrentTransaction == null)
        {
            _fallbackTransaction = _database.BeginTransaction();
        }
    }

    public void Commit()
    {
        _fallbackTransaction?.Commit();
    }

    public void Rollback()
    {
        _fallbackTransaction?.Rollback();
    }
}

Then I have created an extension method:

public static FallbackTransaction BeginFallbackTransaction(this System.Data.Entity.Database database)
{
    var transaction = new FallbackTransaction(database);
    transaction.Begin();
    return transaction;
}

So we can do:

using (var transaction = dbContext.Database.BeginFallbackTransaction())
{
    // do something
    transaction.Commit();
}

This is based on ideas of the answer from jbl

like image 2
Christian Gollhardt Avatar answered Oct 20 '22 02:10

Christian Gollhardt