2017-05-24 24 views
5

W mojej aplikacji .NET Core, mam klasę dekoratorów, które miałem nadzieję, że będą w stanie obsłużyć transakcje poprzez zawijanie wykonywania komend bazy danych w TransactionScope. Niestety, wydaje się, że wsparcie dla TransactionScope nie wejdzie w SqlConnection przez wydanie .NET Core 2: https://github.com/dotnet/corefx/issues/19708:.NET Renderowanie transakcji Core bez TransactionScope

W przypadku braku TransactionScope, nie jestem pewien najlepszego podejścia do tego problemu . Z TransactionScope, mój dekorator transakcja wygląda następująco:

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

Obecnie każda realizacja ICommandHandler dostaje wystąpienie z mojej klasy DapperContext i obsługuje polecenia tak:

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

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

     var sql = Resources.UpdateEntityPart2; 

     //call Execute again 
    } 

Klasa DapperContext ma fabrykę połączeń aby zapewnić nowe połączenia dla każdego wywołania metody Execute. Ponieważ program obsługi komend może być zmuszony do wykonywania wielu zapisów do bazy danych dla pojedynczego TCommand, potrzebuję możliwości wycofania, gdy coś się nie powiedzie. Konieczność tworzenia transakcji w tym samym czasie, w którym tworzę połączenia (w DapperContext) oznacza, że ​​nie mam możliwości zagwarantowania zachowania transakcyjnego między połączeniami.

Ten alternatywny mam uznać nie wydaje się, że wszystko satysfakcjonujące:

  1. Zarządzaj połączeniami i transakcji na poziomie procedury obsługi poleceń, a następnie przekazać te informacje do Wytworny kontekście. W ten sposób wszystkie zapytania dla danej komendy używają tego samego połączenia i transakcji. To może zadziałać, ale nie podoba mi się pomysł obciążania moich komendantów tą odpowiedzialnością. Pod względem ogólnego projektu wydaje się bardziej naturalne, że DapperContext jest miejscem, w którym trzeba się martwić o połączenie.

Moje pytanie brzmi: czy istnieje sposób na zapisanie dekoratora transakcji bez użycia TransactionScope, biorąc pod uwagę obecne ograniczenia SqlConnection w .NET Core? Jeśli nie, to jakie jest najlepsze rozwiązanie, które nie rażąco narusza zasadę pojedynczej odpowiedzialności?

+2

Gdybym był tobą, skomentowałbym ten konkretny problem z Girhubem, by wyjaśnić Microsoftowi, że jest to poważna niezgodność i absolutnie należy to naprawić. Myślę, że jest to poważny problem dla wielu organizacji migrujących, jeśli TransactionScope nie działa w Core 2. – Steven

Odpowiedz

4

Rozwiązaniem mogłoby być stworzenie SqlTransaction jako część dekoratora i przechowywać go w jakimś ThreadLocal lub AsyncLocal dziedzinie, więc jest dostępny dla innych części transakcji gospodarczych, choć nie jest to wyraźnie przekazane . To jest faktycznie to, co robi pod maską (ale bardziej elegancko).

Jako przykład przyjrzeć się tej pseudo kod:

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; 
     } 
    } 
} 

z abstrakcji, że koparki mogą korzystać:

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 
} 

Implementacja dla ITransactionContainer mógłby wyglądać następująco:

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

Zarówno AsyncTransactionContainer, jak i TransactionCommandHandlerDecorator zależy od AsyncLocal<SqlTransaction>. Powinno to być pojedyncze (ta sama instancja powinna zostać wprowadzona do obu).

+0

Doceń porady, Steven. Nie jestem w stanie przetestować tego w tej chwili, ale zastanawiam się, czy to będzie wymagało ode mnie utrzymania tej samej instancji SqlConnection przez wiele wywołań do Execute. Ponieważ SqlTransaction jest połączony z konkretnym połączeniem, nie jestem pewien, co się stanie, jeśli spróbuję wywołać Dappera przy pomocy CurrentTransaction, ale z nowo utworzonego połączenia. – Matt

+0

To oznacza ponowne użycie tego samego SqlConnection. Połączenie musi być również otwarte/zamknięte wewnątrz dekoratora. – Steven

+0

Dwa dodatkowe pytania: Czy istnieje jakikolwiek powód, aby nie dodawać setera do kontenera transakcji, aby dekorator mógł również polegać na interfejsie? Pomyśl, że ułatwiłoby to testowanie jednostek. Czy w tym przypadku Singleton jest konieczny? Czy nie byłoby wielu wystąpień (na przykład używanie stylu AsyncScoped) funkcji AsyncLocal w ten sam sposób, ponieważ instancje tylko odczytują i zapisują do ExecutionContext, z którego są wywoływane? Przy okazji, dla każdego, kto to czyta, struktura kontrolera poleceń/dekoratora, którego używam, pochodzi ze strony Stevena. – Matt