Mendz.Data And Transactions

Mendz.Data represents the team's "core architecture" for creating applications with a database backend. Mendz.Data (and Mendz.Library) is discussed in the Preparing for Dapper and Working with Dapper series. This article is in response to an inquiry about transactions with repositories based on Mendz.Data.

Although I replied directly with sample codes, the comments section removes formatting. Thus, this article repeats most parts in my comment response.

Dapper can be used in .Net Framework and in .Net Core projects. The Mendz.Library and Mendz.Data codes are written to be likewise usable in either .Net Framework or in .Net Core projects. The following assumes that you have created repositories using Mendz.Data.

If you are using .Net Framework, you can wrap the calls to your repositories in a TransactionScope block. Dapper is internally just ADO.Net, which is TransactionScope aware/compatible. Thus, the ff. can be done in the application code:

using (TransactionScope scope = new TransactionScope())
{
    (using FirstRepository r1 = new FirstRepository())
    {
        ...
        (using SecondRepository r2 = new SecondRepository())
        {
            ...
        }
    }
    scope.Complete();
}

If you are using .Net Core, unfortunately as of this writing, TransactionScope is not (fully) supported. It is being considered/developed for future .Net Core releases. Thus, transactions in .Net Core would have to be done inside the aggregate root's repository. For example, in your FirstRepository code:

public First Update(First model, dynamic expansion, 
    out List<ResultInfo> returnValues)
{
    try
    {
        spName = "FirstUpdate";
        BeginTransaction();
        ...
        int affectedCount = DbDataContext.Context.Execute(spName, ..., 
            DbDataContext.Transaction);
        ResultInfo resultInfo = new ResultInfo(spName, affectedCount);
        using (SecondRepository r = new SecondRepository(DbDataContext))
        {
            ...
            r.Create(..., out List<ResultInfo> rv);
            resultInfo.ResultInfos = rv;
        }
        EndTransaction();
        returnValues = new List<ResultInfo> { resultInfo };
        return model;
    }
    catch
    {
        EndTransaction(EndTransactionMode.Rollback);
        throw;
    }
}

Observe that Dapper's Execute() method can accept an IDbTransaction parameter. When using Mendz.Data, it is safe to always pass the DbDataContext.Transaction to Dapper's Execute(). If BeginTransaction() is not called, the DbDataContext.Transaction property returns null. If BeginTransaction() is called, the DbDataContext.Transaction property returns the transaction instance.

The SecondRepository must implement an internal constructor that accepts an IDbDataContext parameter. This constructor allows repositories to share the same database context instance. If SecondRepository is in a separate assembly, it should be set as a friend of FirstRepository's assembly. This is discussed here.

SecondRepository's Create() implementation should also pass the DbDataContext.Transaction to its call to Dapper's Execute() method. This makes sure that the operation will participate in the same transaction started by the FirstRepository.

Kudos to Lajith Kumar who inspired this article.

Comments

Lajith said…
Thanks a lot @medz for your detailed exaplanation
Mendz said…
You're welcome!