Skip to content

igorgomeslima/MongoDB.RepositoryPattern.With.Transaction

Repository files navigation

💚💛💙 PT-BR article here!

C# - Repository Pattern With Support To Transactions on MongoDB.

This solution is divided into two approaches, and aims to implement the Repository pattern with support for sending transactional commands to an MongoDB database.

🦾 Environment Setup

Attention: Currently MongoDB only supports Transactions in clusters with "Replica Set" configured. For this reason, for greater agility in the process of setting up the environment, I recommend you to use the free tier MongoDB Atlas as the even already has this configuration.

  • .NET Core SDK 3.1;
  • MongoDB.Driver v. (2.11.2) or higher;
  • MongoDB v. (4.0) or higher with Replica Set configured;

If you have MongoDB installed locally, you can follow the official documentation to perform this configuration, in this link there are several ramifications for completing the configuration, and if I use Docker I recommend this tutorial here.

📟 To Run

  1. Put your MongoDB connection string on appsettings.Development.json file;
  2. Set the startup project client:
    • Client.WorkerService.FirstApproach
    • Client.WorkerService.SecondApproach
  3. Study the solution; 🤓

🧪 First Approach

This approach shows how to transact commands using only the context implementation(DbContext) created for the database, in this case, IMongoDbContextFirstApproach. In this implementation it's not necessary another logic to manage commands that are inside a created transaction(it replaces something that would act as Unit Of Work).

Approach considerations:

  • The IMongoDbContextFirstApproach interface acts as Unit Of Work;
  • The IMongoDbContextFirstApproach interface is responsible for the creation of a Transaction(create the scope), in case the commands need to be involved in a Transaction;
  • The IMongoDbContextFirstApproach interface is responsible to manage the current Transaction of the scope created;
  • The IMongoDbContextFirstApproach interface is responsible for sending the commands to Database;
  • The IMongoDbContextFirstApproach interface is injected on "services" where the Database commands need to be transacted;

🧪 Second Approach

This approach shows how to transact commands using the Unit Of Work approach in fact. There are some points that we should consider when trying to create a logic to manage transactions using Unit Of Work in MongoDB, and I will talk about this on final considerations.

Approach considerations:

  • The IUnitOfWork interface is responsible for requesting the creation of a Transaction(creating the scope), case commands need to be involved in one;
  • The IUnitOfWork interface is responsible to manage the current Transaction of the created scope;
  • The IUnitOfWork interface is responsible for sending commands to Database;
  • The IUnitOfWork interface is injected into "services" where Database commands need to be transacted;
  • The implementation of the IUnitOfWork interface is directly dependent on the Driver used to connect to MongoDB;
  • The IMongoDbContextSecondApproach interface is responsible to receive and resolve requests from Unit Of Work;

As we can see, the implementation of the IUnitOfWork interface is not self-sufficient, as it depends on IMongoDbContextSecondApproach to be able create/manage a Transaction.

💭 Final Considerations

First of all, my "favorite" approach is the "First", because the class MongoDbContextFirstApproach that manage connections and transactions is completely self-sufficient, that is, it depends only on your resources to perform yours operations, like: BeginTransaction, Commit. Etc.... Still, this approach is not a "silver bullet".

Discussing a little about the "Second Approach", we realized that IUnitOfWork is not self-sufficient(as already mentioned). It is completely dependent of IMongoDbContextSecondApproach interface. Because it does not have the "Driver" resources, in this case the IMongoClient resource required to create a Transaction, so it needs to "request" these resources for a service that has them, in this case, IMongoDbContextSecondApproach.

Ultimately IUnitOfWork acts as "by pass" of Transaction. Maybe it helps with the fact that you don't need to inject DbContext into services that need a Transaction, but in my opinion, injecting a DbContext into a service is not a problem.

Last caveats about this solution...

First...

The implementation carried out to support Transactions in this example project were made in an attempt to meet the need that the official MongoDB Driver design has, which in this case is:

It's necessary inform at the moment of the creation command (Insert, Delete, Update) if it will belong in a Transaction, and if it belongs, the Transaction needs to be informed through a parameter(IClientSessionHandle session) of the respective Driver command.

For example:

  • Collection.InsertOneAsync(session: transaction, ...);
  • Collection.InsertManyAsync(session: transaction, ...);
  • Collection.UpdateOneAsync(session: transaction, ...);

In this scenario, we are not able to "create a magic scope", like TransactionScope, and "inside it" execute the commands that will be part of our Transaction. There is a request to include this feature in the official Driver here.

Second...

I still think that implementing the Repository & Unit Of Work pattern "on top" of the features offered by the official Driver connection, will imply the same criticisms that are made when we think about doing the same implementation "on top" of the Entity Framework Core. For example, IMongoCollection<TDocument> already acts like a "repository" for us, because have Find, Insert, Update, Delete, etc... methods. This is equivalent to EF Core DbSet<TEntity>(which comes under criticism from a large part of the community).

I recommend some articles for reflection:

[Entity Framework Approach]

And of course I recommend the series of articles from Brian Bu, where he puts important arguments in this discussion:

[MongoDB Approach]

Third(is not the focus of the solution)...

The way the IUnitOfWork interface was implemented enables "switch databases" only modifying the class that implements it in our dependency injection container.

For example:

  • services.AddScoped<IUnitOfWork, UnitOfWorkPostgreSQL>
  • services.AddScoped<IProductRepository, ProductRepositoryPostgreSQLFirstApproach>();
  • services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepositoryPostgreSQLFirstApproach<>));

OR

  • services.AddScoped<IUnitOfWork, UnitOfWorkMySql>
  • services.AddScoped<IProductRepository, ProductRepositoryMySqlFirstApproach>();
  • services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepositoryMySqlFirstApproach<>));

P.S. Please understand "switch database" as an exchange for the Driver that their repositories use, as each database provider handles a Transaction differently, thus our concrete class implementation of IUnitOfWork needs to change according to the specificity of the new assigned database.

If you find any failure/problems or have knowledge to improve this solution, I kindly ask you to contact me, either by E-mail, Pull Request or Issue.

:)

Resources used

Working with MongoDB Transactions with C# and the .NET Framework

About

C# - Repository Pattern With Support To Transactions on MongoDB.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages