2016-01-12 18 views
5

Sfondo: Ho un servizio WCF con SimpleInjector come IoC che crea un'istanza di DbContext per richiesta WCF.Come verificare se DbContext ha una transazione?

Il backend stesso è CQRS. CommandHandlers hanno un sacco di decoratori (convalida, autorizzazione, la registrazione, alcune regole comuni per i diversi gruppi di gestore, ecc) e uno di loro è Transaction Decorator:

public class TransactionCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand> 
    where TCommand : ICommand 
{ 
    private readonly ICommandHandler<TCommand> _handler; 
    private readonly IMyDbContext _context; 
    private readonly IPrincipal _principal; 

    public TransactionCommandHandlerDecorator(ICommandHandler<TCommand> handler, 
     IMyDbContext context, IPrincipal principal) 
    { 
     _handler = handler; 
     _context = context; 
     _principal = principal; 
    } 

    void ICommandHandler<TCommand>.Handle(TCommand command) 
    { 
     using (var transaction = _context.Database.BeginTransaction()) 
     { 
      try 
      { 
       var user = _context.User.Single(x => x.LoginName == _principal.Identity.Name); 
       _handler.Handle(command); 
       _context.SaveChangesWithinExplicitTransaction(user); 
       transaction.Commit(); 
      } 
      catch (Exception ex) 
      { 
       transaction.Rollback(); 
       throw; 
      } 
     } 
    } 
} 

problema si verifica quando eventuali tentativi di comando a catena eseguire un altro comando all'interno della stessa richiesta WCF. Ho trovato un'eccezione prevista in questa linea:

using (var transaction = _context.Database.BeginTransaction()) 

perché il mio esempio DbContext ha già una transazione.

Esiste un modo per verificare l'esistenza della transazione corrente?

+2

Ti manca una [astrazione olistica] (http://qujck.com/commands-and-queries-are-holistic-abstractions/) – qujck

+0

@qujck dopo aver letto questo articolo non capisco quale sia la differenza tra IQuery 'e' IDataQuery '. Entrambi restituiscono i dati. Perché abbiamo bisogno di una seconda interfaccia per questo? Ci sono degli esempi? – Szer

+0

Entrambi restituiscono dati ma queste diverse astrazioni sono importanti solo quando si tratta di decorare il codice. Hai bisogno di un'astrazione che possiede l'intera operazione, un'astrazione che può essere avvolta con qualcosa come un "TransactionDecorator". Avete altre astrazioni di livello inferiore che possono essere decorate con preoccupazioni trasversali come 'AuthoriseDecorator' o' LoggingDecorator' che non riguardano l'operazione atomica. – qujck

risposta

4

Anziché utilizzare la transazione dal DbContext di Entity Framework, è possibile o forse necessario utilizzare la classe TransactionScope che crea un ambito di transazione ambientale e gestisce le transazioni di tutte le connessioni effettuate al database (SQL) sotto le copertine.

Si potrebbe anche inserire un valore SqlCommand diretto nella stessa transazione se si utilizza il connettore di connessione esatto (con distinzione tra maiuscole e minuscole) per SqlCommand. I messaggi scritti su MessageQueue sono anche incapsulati nella stessa transazione

È anche possibile gestire le connessioni a diversi database allo stesso tempo. Per questo utilizza il servizio Windows DTC. Fai attenzione che questo è un problema da configurare se necessario. Normalmente, con una singola connessione DB (o più connessioni allo stesso DB) non è necessario il DTC.

Il TransactionScopeCommandHandlerDecorator implementazione è banale:

public class TransactionScopeCommandHandlerDecorator<TCommand> 
     : ICommandHandler<TCommand> 
{ 
    private readonly ICommandHandler<TCommand> decoratee; 

    public TransactionScopeCommandHandlerDecorator(ICommandHandler<TCommand> decoratee) 
    { 
     this.decoratee = decoratee; 
    } 

    public void Handle(TCommand command) 
    { 
     using (var scope = new TransactionScope()) 
     { 
      this.decoratee.Handle(command); 

      scope.Complete(); 
     } 
    } 
} 

Ma: Come qujck già accennato nei commenti, vi manca il concetto di ICommandHandler come un'operazione atomica. Un comandante non dovrebbe mai fare riferimento a un altro comandante. Non solo è male per le transazioni, ma considera anche questo:

Immagina che l'applicazione cresca e che tu possa refactoring alcuni dei tuoi commandhandler su un thread in background, che verrà eseguito in alcuni servizi di Windows. In questo servizio Windows non è disponibile uno stile di vita PerWcfOperation. Avresti bisogno di uno stile di vita LifeTimeScope per voi comandanti ora. Dal momento che il tuo design lo consente, il che è davvero fantastico !, tipicamente avvolgi i tuoi comandanti in un LifetimeScopeCommandHandler decorator per avviare LifetimeScope. Nel tuo progetto attuale in cui un singolo comandante di comandi fa riferimento ad altri comandanti che si imbatteranno in un problema, poiché ogni comandante sarà creato nel suo ambito, ottiene così un altro DbContext iniettato rispetto agli altri comandanti!

Quindi è necessario riprogettare e rendere i vostri commandhandler holistic abstractions e creare un'astrazione di livello inferiore per eseguire le operazioni DbContext.

+0

Questo è l'approccio giusto, +1. 'Potrebbe anche mettere un SqlCommand diretto nella stessa transazione se si usasse la connessione di connessione esatta (con distinzione tra maiuscole e minuscole) per SqlCommand' questo non è garantito. Attento, questo tende a fallire sotto carico. – usr

+0

@usr, sei sicuro? Immagino che tu sia. Puoi indicare qualche documentazione su questo? In alcuni scenari ci basiamo molto su questo ... Grazie mille per questa nota! –

+0

@ Ric.Net molte grazie! Prenderò in considerazione la riprogettazione dei comandi a catena. Ma dovrei dire che non è facile. Nel mio caso, quando il client invia il comando AddEntityA, il server dovrebbe aggiungere l'entità A al database e dopo di ciò dovrebbe anche aggiungere l'entità B. Il comando AddEntityB ha un sacco di regole e controlli e proprio decoratore quindi se l'entità B non può essere creata l'entità A anche non sarà aggiunto al database. Ecco perché eseguo il comando all'interno del comando – Szer

10

penso che stai cercando il CurrentTransaction proprietà del DbContext:

var transaction = db.Database.CurrentTransaction; 

allora si può fare una verifica del genere:

using(var transaction = db.Database.CurrentTransaction ?? db.Database.BeginTransaction()) 
{ 
    ... 
} 

Tuttavia io non sono sicuro di come si può sapere quando eseguire il commit della transazione se è utilizzata da metodi concorrenti.

+0

Rispondo molto alla tua risposta perché per qualche motivo ho avuto la versione 6.0.0 di EF che non ha questa proprietà. Dopo l'Update-Package EF va a 6.1.3 e la proprietà è lì. Grazie molto! – Szer

+0

@Szer sì non è stato in EF per molto tempo :) –

+0

Mi rendo conto che la tua risposta è "risposta" più semplice alla mia domanda, ma dovrei ammettere che la vera risposta è "cattiva progettazione" che è ciò che @ Ric.Net disse. – Szer