没有TransactionScope的.Net核心事务修饰器

Mat*_*att 5 .net transactions .net-core

在我的.NET Core应用程序中,我有一个装饰器类,我希望通过在TransactionScope中包装数据库命令的执行来处理事务.不幸的是,似乎支持TransactionScope不会通过.NET Core 2的发布进入SqlConnection:https://github.com/dotnet/corefx/issues/19708:

在缺少TransactionScope的情况下,我不确定解决此问题的最佳方法.使用TransactionScope,我的事务装饰器如下所示:

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();
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

目前,ICommandHandler的每个实现都获取了我的DapperContext类的实例,并处理如下命令:

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

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

        var sql = Resources.UpdateEntityPart2;

        //call Execute again
    }
Run Code Online (Sandbox Code Playgroud)

DapperContext类有一个连接工厂,为每次调用Execute方法提供新的连接.因为命令处理程序可能必须为单个TCommand执行多个数据库写入,所以我需要能够在出现故障时回滚.必须在我创建连接的同时创建事务(在DapperContext中)意味着我无法保证跨连接的事务行为.

我考虑过的另一种选择似乎并不令人满意:

  1. 在命令处理程序级别管理连接和事务,然后将该信息传递给dapper上下文.这样,给定命令的所有查询都使用相同的连接和事务.这可能有用,但我不喜欢给我的命令处理程序加负担这个责任的想法.在整体设计方面,似乎更自然的是DapperContext是担心连接的地方.

我的问题是:考虑到.NET Core中SqlConnection的当前限制,有没有办法在不使用TransactionScope的情况下编写事务装饰器?如果没有,那么下一个最好的解决方案是什么,并不会过于严重地违反单一责任原则?

Ste*_*ven 4

解决方案可能是创建 aSqlTransaction作为装饰器的一部分,并将其存储在某种ThreadLocalAsyncLocal字段中,以便它可用于业务事务的其他部分,即使它没有显式传递。这实际上是TransactionScope在幕后所做的事情(但更优雅)。

例如,看一下这个伪代码:

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;
       }
   }
}
Run Code Online (Sandbox Code Playgroud)

通过处理程序可以使用的抽象:

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
}
Run Code Online (Sandbox Code Playgroud)

的实现ITransactionContainer可能如下所示:

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.");
}
Run Code Online (Sandbox Code Playgroud)

和 都AsyncTransactionContainer依赖TransactionCommandHandlerDecoratorAsyncLocal<SqlTransaction>. 这应该是一个单例(同一个实例应该注入到两者中)。