.Net Core Transaction Handler without TransactionScope

In my .NET Core application, I have a decorator class that I hope will be able to process transactions, completing the execution of database commands in TransactionScope. Unfortunately, it seems that TransactionScope support is not going to turn it into SqlConnection with the release of .NET Core 2: https://github.com/dotnet/corefx/issues/19708 :

In the absence of TransactionScope, I am not sure of a better approach to this problem. With TransactionScope, my transaction handler 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 ICommandHandler implementation receives an instance of my DapperContext class and processes the following commands:

 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 factory connection to provide new connections for each call to its Execute method. Since the command handler may have to execute multiple database records for one TCommand, I need the ability to rollback on failures. When creating transactions simultaneously with creating connections (in DapperContext) means that I can not guarantee transactional behavior in all connections.

The only alternative I considered seems not so pleasant:

  • Manage connections and transactions at the command handler level, and then pass this information to the dapper context. Thus, all requests for this command use the same connection and transaction. This might work, but I don't like the idea of ​​burdening my handlers with this responsibility. Regarding the overall design, it seems more natural that the DapperContext will be a place to worry about communication.

My question is: is it possible to write a transaction constructor without using TransactionScope, given the current limitations of SqlConnection in .NET Core? If not, what is the best solution that does not violate the principle of single responsibility is too egregious?

+5
source share
1 answer

The solution could be to create SqlTransaction as part of the decorator and save it in some ThreadLocal or AsyncLocal , so it is available for other parts of the business transaction, although it is not explicitly transferred. This is effectively what TransactionScope does under the cover (but more elegantly).

As an example, consider 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 } 

The implementation for ITransactionContainer might 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 AsyncTransactionContainer and TransactionCommandHandlerDecorator are dependent on AsyncLocal<SqlTransaction> . This must be a singleton (the same instance must be entered in both).

+4
source

Source: https://habr.com/ru/post/1268181/


All Articles