Qualche giorno fa ho avuto modo di (s)parlare di come poter implementare IEnlistmentNotification in una propria classe di business al fine di realizzare un resource manager che sia utilizzabile all’interno di un TransactionScope. Quello che però ho dato per scontato è che si sappia come funziona il giochino. Ho già avuto modo di parlarne ampiamente in un articolo per MSDN ma alcuni dettagli forse mancano ed è giusto riprenderli qui, partiamo da questo snippet:
using( TransactionScope scope = new TransactionScope() )
{
    //Code inside a transaction….
    scope.Complete();
}

la prima nota da fare è che la Dispose di TransactionScope non è una vera e propria Dispose() ma serve principalmente per concludere la transazione con un Rollback(), nel caso si sia verificata una exception e/o la Complete() non sia stata chiamata, o con una Commit() in caso di successo. Approfondendo ulteriormente quello che succede in caso di successo è:
  • viene invocato TransactionScope.Complete();
  • il transaction a manger si segna che la transazione è completata;
  • scorre i “vari” resource manager “enlisted” nella transazione e chiama Prepare/Commit o SinglePhaseCommit a seconda del tipo di enlistment;
  • si conclude definitivamente la transazione;
Se infatti realizziamo un’implementazione banalissima in una applicazione console:
MyResourceManager rm = new MyResourceManager();
using( TransactionScope tx = new TransactionScope() )
{
    Console.WriteLine( "TransactionScope created. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );
    rm.DoWork();
    tx.Complete();
    Console.WriteLine( "Complete() called inside using block. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );
}

Console.WriteLine( "Dispose() called outside using block. ThreadId: {0}", Thread.CurrentThread.ManagedThreadId );
dove MyResourceManager implementa IEnlistmentNotification, notiamo che:
image
Prepare e Commit sono successivi alla chiamata a Complete(), quindi un paio di tip:
  1. Non c’è nessuna garanzia che Prepare/Commit vengano chiamati nello stesso thread che sta gestendo la transazione, quindi non fate il benchè minimo affidamento su questo.
  2. Commit() e Prepare() non possono assolutamente fallire pena nessun Rollback(), per essere precisi all’interno di prepare avete l’ultima possibilità di forzare un Rollback(); attraverso preparingEnlistment.ForceRollback() peccatro che non funzioni neanche a pagarlo… indagando un po’ più a fondo, cioè senza usare MSDN… :-(, si scopre che lo scenario deve essere quello di una transazione distributia e il nostro resource manager deve essere un durable resource manager… cioè tutto un’altro pianeta ;-)
    Sta di fatto che quindi la Commit() non deve fallire, il lavoro deve essere fatto all’interno della DoWork() e li devo:
    1. Eseguire l’enlistment nella transazione corrente, se presente;
    2. eseguire le operazioni chje devo eseguire tenendo traccia dei singoli passaggi fatti in modo da poter fare un Rollback() granulare e preciso;
    3. eseguire il tutto eventualmente su variabili/risorse temporanee per rispettare i principi di isolamento;
    4. Nella Commit() confermare le operazioni fatte rendendo persistenti e visibili all’esterno le modifiche;
    5. nell’eventulae Rollback() ripristinare lo stato precedente l’inizio della transazione;
  3. Terza nota, non proprio semplicissima da implementare: il vostro resource manager deve rispettare il livello di Isolamento che c’è impostato sulla transazione e quindi ad esempio acquisire lock sulle risorse o mascherare/consentire la lettura dei valori in maniera diversa a seconda che il chimante sia all’interno della transazione o meno… mi aspetto per cui che in uno scenario come questo:
  4. TransactionOptions op = new TransactionOptions();
    op.IsolationLevel = System.Transactions.IsolationLevel.Serializable;
    using( TransactionScope tx = new TransactionScope( TransactionScopeOption.RequiresNew, op ) )
    {
        IPerson aPerson = Repository.GetPerson();
        aPerson.Name = "Mauro";

        tx.Complete();
    }

    la lettura di aPerson.Name all’interno della transazione ritorni “Mauro” mentre se un thread esterno cercasse di leggere verrebbe accodato in attesa del completamento della transazione o che la risorsa si comportasse in base al livello di isolamento impostato.
    Per raggiungere questo scopo è decisamente più semplice se l’implementazione di IEnlistmentNotification la fate direttamente sulla risorsa in questione (IPerson in questo caso, sul PersonProxy per essere pignoli) piuttosto che su un terzo attore quale potrebbe essere una ipotetica classe PersonResourceManager.

.m