Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Repository pattern with MongoDB - multiple units of work with one transaction

I'm implementing a DAL abstraction layer on top of the C# Mongo DB driver using the Repository + Unit of Work pattern. My current design is that every unit of work instance opens (and closes) a new Mongo DB session. The problem is that Mongo DB only allows a 1:1 ratio between a session and a transaction, so multiple units of work under the same .NET transaction will not be possible.

The current implementation is:

public class MongoUnitOfWork
{
    private IClientSessionHandle _sessionHandle;

    public MongoUnitOfWork(MongoClient mongoClient)
    {
       _sessionHandle = mongoClient.StartSession();
    }

    public void Dispose()
    {
       if (_sessionHandle != null)
       {
          // Must commit transaction, since the session is closing
          if (Transaction.Current != null)
            _sessionHandle.CommitTransaction();
          _sessionHandle.Dispose();
       }
    }
}

And because of this, the following code won't work. The first batch of data will be committed ahead of time:

using (var transactionScope = new TransactionScope())
{
    using (var unitOfWork = CreateUnitOfWork())
    {
       //... insert items

       unitOfWork.SaveChanges();
    }  // Mongo DB unit of work implementation will commit the changes when disposed

    // Do other things

    using (var unitOfWork = CreateUnitOfWork())
    {
       //... insert some more items
       unitOfWork.SaveChanges();
    }
    transactionScope.Complete();
}

Obviously the immediate answer would be to bring all of the changes into one unit of work, but it's not always possible, and also this leaks the Mongo DB limitation.

I thought about session pooling, so that multiple units of work will use the same session, and commit/rollback when the transient transaction completes/aborts.

What other solutions are possible?

Clarification:

The question here is specifically regarding Unit-Of-Work implementation over MongoDB using the MongoDB 4.0 (or later) built-in transaction support.

like image 425
Metheny Avatar asked Apr 28 '19 06:04

Metheny


2 Answers

I never used MongoDB; do not know anything about it. I am only answering in terms of TransactionScope; so not sure if this will help you.

Please refer the Magic Of TransactionScope. IMO, there are three factors you should look for:

  1. Connection to the database should be opened inside the TransactionScope.

    So remember, the connection must be opened inside the TransactionScope block for it to enlist in the ambient transaction automatically. If the connection was opened before that, it will not participate in the transaction.

    Not sure but it looks that you can manually enlist the connection opened outside the scope using connection.EnlistTransaction(Transaction.Current).

    Looking at your comment and the edit, this is not an issue.

  2. All operations should run on same thread.

    The ambient transaction provided by the TransactionScope is a thread-static (TLS) variable. It can be accessed with static Transaction.Current property. Here is the TransactionScope code at referencesource.microsoft.com. ThreadStatic ContextData, contains the CurrentTransaction.

    and

    Remember that Transaction.Current is a thread static variable. If your code is executing in a multi-threaded environments, you may need to take some precautions. The connections that need to participate in ambient transactions must be opened on the same thread that creates the TransactionScope that is managing that ambient transaction.

    So, all the operations should run on same thread.

  3. Play with TransactionScopeOption (pass it to constructor of TransactionScope) values as per your need.

    Upon instantiating a TransactionScope by the new statement, the transaction manager determines which transaction to participate in. Once determined, the scope always participates in that transaction. The decision is based on two factors: whether an ambient transaction is present and the value of the TransactionScopeOption parameter in the constructor.

    I am not sure what your code expected to do. You may play with this enum values.

As you mentioned in the comment, you are using async/await.

Lastly, if you are using async/await inside the TransactionScope block, you should know that it does not work well with TransactionScope and you might want to look into new TransactionScope constructor in .NET Framework 4.5.1 that accepts a TransactionScopeAsyncFlowOption. TransactionScopeAsyncFlowOption.Enabled option, which is not the default, allows TransactionScope to play well with asynchronous continuations.

For MongoDB, see if this helps you.

like image 117
Amit Joshi Avatar answered Nov 07 '22 00:11

Amit Joshi


The MongoDB driver isn't aware of ambient TransactionScopes. You would need to enlist with them manually, or use JohnKnoop.MongoRepository which does this for you: https://github.com/johnknoop/MongoRepository#transactions

like image 27
John Knoop Avatar answered Nov 06 '22 23:11

John Knoop