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:
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?
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With