Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net Core transaction decorator without TransactionScope

Tags:

In my .NET Core application, I have a decorator class that I hoped would be able to handle transactions by wrapping the execution of database commands in a TransactionScope. Unfortunately, it appears that support for TransactionScope isn't going to make it into SqlConnection by the release of .NET Core 2: https://github.com/dotnet/corefx/issues/19708:

In the absence of TransactionScope, I'm not sure of the best approach to this problem. With TransactionScope, my transaction decorator looks like this:

public class TransactionCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;

    //constructor        

    public void Handle(TCommand command)
    {  
       using (var scope = new TransactionScope())
       {
           this.decorated.Handle(command);

           scope.Complete();
       }
    }
}

Currently, each implementation of ICommandHandler gets an instance of my DapperContext class and handles commands like this:

public void Handle(UpdateEntity command)
    {
        var sql = Resources.UpdateEntityPart1;

        this.context.Execute(sql, new
        {
           id = command.Id;             
        });

        var sql = Resources.UpdateEntityPart2;

        //call Execute again
    }

The DapperContext class has a connection factory to provide new connections for each call to its Execute method. Because the command handler may have to to perform multiple database writes for a single TCommand, I need the ability to rollback when something fails. Having to create transactions at the same time that I create connections (in DapperContext) means I have no way to guarantee transactional behavior across connections.

The one alternative I've considered doesn't seem all that satisfying:

  1. Manage connections and transactions at the command handler level, then pass that information to the dapper context. That way all queries for a given command use the same connection and transaction. This might work, but I don't like the idea of burdening my command handlers with this responsibility. In terms of overall design, it seems more natural that the DapperContext be the place to worry about having a connection.

My question, then: Is there any way to write a transaction decorator without the use of TransactionScope, given the current limitations of SqlConnection in .NET Core? If not, what's the next best solution that doesn't violate the principle of single responsibility too egregiously?

like image 669
Matt Avatar asked May 24 '17 16:05

Matt


1 Answers

A solution could be to create a SqlTransaction as part of the decorator, and store it in some sort of ThreadLocal or AsyncLocal field, so it is available for other parts of the business transaction, even though it is not explicitly passed on. This is effectively what TransactionScope does under the cover (but more elegantly).

As example, take a look at this pseudo code:

public class TransactionCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly AsyncLocal<SqlTransaction> transaction;

    public void Handle(TCommand command)
    {
        transaction.Value = BeginTranscation();

       try
       {
            this.decorated.Handle(command);

            transaction.Value.Commit();
       }
       finally
       {
           transaction.Value.Dispose();
           transaction.Value = null;
       }
   }
}

With an abstraction that handlers can use:

public interface ITransactionContainer
{
    SqlTransaction CurrentTransaction { get; }
}


public void Handle(UpdateEntity command)
{
    // Get current transaction
    var transaction = this.transactionContainer.CurrentTransaction;

    var sql = Resources.UpdateEntityPart1;

    // Pass the transaction on to the Execute 
    // (or hide it inside the execute would be even better)
    this.context.Execute(sql, transaction, new
    {
       id = command.Id;             
    });

    var sql = Resources.UpdateEntityPart2;

    //call Execute again
}

An implementation for ITransactionContainer could look something like this:

public class AsyncTransactionContainer : ITransactionContainer
{
    private readonly AsyncLocal<SqlTransaction> transaction;

    public AsyncTransactionContainer(AsyncLocal<SqlTransaction> transaction)
    {
        this.transaction = transaction;
    }

    public SqlTransaction CurrentTransaction =>
        this.transaction.Value
            ?? throw new InvalidOperationException("No transaction.");
}

Both the AsyncTransactionContainer and TransactionCommandHandlerDecorator depend on an AsyncLocal<SqlTransaction>. This should be a singleton (the same instance should be injected into both).

like image 117
Steven Avatar answered Sep 30 '22 00:09

Steven