10

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.

Metheny
  • 1,112
  • 1
  • 11
  • 23
  • Why the downvote? I will be connecting TransactionScope to MongoDB using the transaction manager mechanism. In addition, each unit of work will be updating multiple collections. I can't see exactly how the link solves this issue, the answer in that link practically suggests manually implementing a transaction mechanism manually: the last operation marks the document as complete, but that raises a few questions - how does an action know that it's the last operation? how to perform cleanup, etc. – Metheny Apr 29 '19 at 10:56
  • Yes, in the constructor (added above). Let's assume it all runs on the same thread (I still need to check regarding async/await with ConfigureAwait(false) but let's leave it for now). – Metheny Apr 29 '19 at 11:57
  • Using TransactionScope in the MongoDB context like you do is useless as C# driver doesn't use the `System.Transactions` namespace at all. Beyond that consideration, TransactionScope is based on the two-phase commit protocol, and MongoDB has a loose relation with this https://docs.mongodb.com/v3.4/core/write-operations-atomicity/#transaction-like-semantics. My advice is to stick with what the C# driver offers (Session). If you really want TransactionScope, then you can look at this https://www.codeproject.com/Articles/1104719/MongoDB-and-System-Transactions (still, I'm unsure of its usefulness) – Simon Mourier May 01 '19 at 07:59
  • @Simon Mourier: thanks, but as I wrote in the other comments, I'm already aware of the transaction manager mechanism and am going to use it to connect TransactionScope with Mongo DB. As I understand, the resource manager can choose to implement ISinglePhaseNotification. See my answer to Amit which also provided the same link. In addition, I'm implementing a DAL layer which abstracts Mongo DB, so the service layer cannot directly work with the Mongo DB driver. – Metheny May 01 '19 at 08:45
  • In my understanding, UnitOfWork is used to save several entities at the same time, which creates the effect of a transaction. Why do you want to have both the Transaction and UnitOfWork concepts at the same time? That is confusing because the two mean about the same thing. The transaction is a more technical term, that's all. – Tengiz May 01 '19 at 20:44
  • @Tengiz: In Entity Framework for example the DbContext is the Unit of Work, which has a SaveChanges method, but if wrapped in a Transaction, can still rollback after SaveChanges was called. Sometimes you cannot make all the changes in one place, or a service makes some of the changes then calls another service which performs additional changes, so each of them would have its own UOE, but a transaction will wrap both so that we get the all-or-nothing behavior. Not to mention a distributed transaction between different machines and different DBs, where each creates UOWs against its own DB. – Metheny May 01 '19 at 20:59
  • I see. I understand your point now. Have you considered the validity of your approach against the repository pattern that discourages saving more than one aggregate at a time? I've never used transactions in EF probably because I never designed repository in the way that you described. – Tengiz May 01 '19 at 23:20
  • @Tengiz: Not sure I understand, can you please explain what is one aggregate at a time? – Metheny May 02 '19 at 04:59
  • @Metheny: In DDD world, repository should flush only one aggregate (entity (nested may be)) at a time. – Amit Joshi May 02 '19 at 13:21
  • Yes, as Amit said, I meant that it's recommended that the enterprise applications implement repository so that it saves/retrieves one aggregate at a time with each operation. I would not say that it's limited to DDD only, but DDD does provide the same guidance. Aggregate is a set of entities that together define the transactional consistency boundary. i.e., each aggregate is already a transaction so no need to worry about larger-than-aggregate transactions. – Tengiz May 02 '19 at 13:46
  • There can be multiple saves (units of work) for the same aggregate *type*, as in the scenarios I've mentioned in the previous comment (multiple services which need to save data, or a distributed transaction which needs to save the same (or different) data in different databases). – Metheny May 02 '19 at 14:03

2 Answers2

6

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.

Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
  • Thanks for your answer. I have already read about the transaction manager and how to connect TransactionScope to my implementation. My question is more specific to Mongo DB. The link you provided relates to Mongo DB prior to their support of multi-document transactions and involves manually implementing the transaction mechanism, whereas I'm hoping to make use of the built-in transaction support of Mongo DB 4.0 which was recently added. – Metheny Apr 29 '19 at 12:42
  • You said (in your comment) you are using `async/await` which looks that does not work correctly with Transaction Scope. All points I mentioned in answer are irrespective of database (RDBMS or document-oriented). I though it will at least point you in the direction. Alas! I can not be of any help further. – Amit Joshi Apr 29 '19 at 12:47
  • Thanks, I mentioned the threading issue in regard to your question, but the main obstacle for me is the limitation due to the relationship between the Mongo session & Mongo transaction. In any case, thank you for your efforts, much appreciated. – Metheny Apr 29 '19 at 12:58
3

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

John Knoop
  • 605
  • 5
  • 16