2017-05-24 58 views
5

在我的.NET Core應用程序中,我有一個裝飾器類,我希望能夠通過在TransactionScope中包裝數據庫命令的執行來處理事務。不幸的是,對於TransactionScope的支持似乎不會通過.NET Core 2的發佈將其轉換爲SqlConnection:https://github.com/dotnet/corefx/issues/19708.Net沒有TransactionScope的核心事務裝飾器

在沒有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(); 
     } 
    } 
} 

目前,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 
    } 

的DapperContext類有一個連接工廠爲每次調用Execute方法提供新的連接。因爲命令處理程序可能必須爲單個TCommand執行多個數據庫寫入操作,所以我需要在出現問題時能夠回滾。必須在創建連接的同時創建事務(在DapperContext中)意味着我無法保證跨連接的事務行爲。

的一個替代方案,我認爲似乎並沒有全部滿足:

  1. 在命令處理程序級別管理連接和事務,然後將該信息傳遞到短小精悍的上下文。這樣,給定命令的所有查詢都使用相同的連接和事務。這可能會起作用,但我不喜歡讓我的命令處理程序負擔這個責任。就整體設計而言,DapperContext成爲擔心連接的地方似乎更自然。

我的問題是:有沒有什麼辦法可以在沒有使用TransactionScope的情況下編寫事務裝飾器,因爲.NET Core中的SqlConnection的當前限制?如果不是,下一個最佳解決方案是不是太違反單一責任原則?

+2

如果我是你,我會對那個特殊的Girhub問題發表評論,向微軟解釋這是一個主要的不兼容問題,絕對應該修復。如果TransactionScope在Core 2中不起作用,我認爲這對許多遷移組織來說是一個主要問題。 – Steven

回答

4

一個解決方案是創建一個SqlTransaction作爲裝飾的一部分,並將其存儲在某種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; 
     } 
    } 
} 

隨着抽象處理程序可以使用:

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 
} 

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."); 
} 

AsyncTransactionContainerTransactionCommandHandlerDecorator取決於AsyncLocal<SqlTransaction>。這應該是一個單例(同一個實例應該被注入到這兩者中)。

+0

欣賞Steven的建議。我目前無法對此進行測試,但我想知道這是否會要求我跨多個調用執行相同的SqlConnection實例。由於SqlTransaction鏈接到特定的連接,我不確定如果我嘗試使用CurrentTransaction調用Dapper,但是從新創建的連接調用Dapper會發生什麼情況。 – Matt

+0

這意味着重用相同的SqlConnection。連接還需要在裝飾器中打開/關閉。 – Steven

+0

如果您願意,還有兩個問題:是否有任何理由不將添加器添加到事務容器中,以便修飾器也可以依賴於接口?認爲這將使單元測試更容易。另外,在這種情況下Singleton是否必要? AsyncLocal的多個實例(例如使用AsyncScoped生活方式)不會以同樣的方式運行,因爲實例只會讀取和寫入從中調用它們的ExecutionContext?順便說一句,對於讀這個的人來說,我使用的commandhandler/decorator結構來自Steven的網站。 – Matt