💚💛💙 PT-BR article here!
This solution is divided into two approaches, and aims to implement the Repository pattern with support for sending transactional commands to an MongoDB database.
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.
- Put your MongoDB connection string on
appsettings.Development.json
file; - Set the startup project client:
- Client.WorkerService.FirstApproach
- Client.WorkerService.SecondApproach
- Study the solution; 🤓
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 asUnit Of Work
; - The
IMongoDbContextFirstApproach
interface is responsible for the creation of aTransaction
(create the scope), in case the commands need to be involved in aTransaction
; - The
IMongoDbContextFirstApproach
interface is responsible to manage the currentTransaction
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;
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 aTransaction
(creating the scope), case commands need to be involved in one; - The
IUnitOfWork
interface is responsible to manage the currentTransaction
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 theDriver
used to connect to MongoDB; - The
IMongoDbContextSecondApproach
interface is responsible to receive and resolve requests fromUnit 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
.
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 aTransaction
, and if it belongs, theTransaction
needs to be informed through a parameter(IClientSessionHandle session
) of the respectiveDriver
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]
- # Is the repository pattern useful with Entity Framework Core?
- # Repositories On Top UnitOfWork Are Not a Good Idea
- # No need for repositories and unit of work with Entity Framework Core
- # Quando usar Entity Framework com Repository Pattern? [PT-BR]
And of course I recommend the series of articles from Brian Bu, where he puts important arguments in this discussion:
- # The Repository Pattern isn’t an Anti-Pattern; You’re just doing it wrong.
- # Typical Anti-Repository Arguments
- # Repository Pattern: Retrospective and Clarification
[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