Dunque, supponiamo di avere il seguente stralcio di codice:
IChangeTrackingServiceContainer container = ChangeTrackingServiceContainer.GetProvider();
IChangeTrackingService tracking = container.CreateTrackingService();

DataContextProcess dataContext = new DataContextProcess();
IEntityCollection allPersons = dataContext.GetAll();

IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();
first.FirstName = "Mauro";

if( tracking.IsChanged )
{
    IEnumerable advisory = tracking.GetAdvisory();
    dataContext.Commit( advisory );
    tracking.AcceptChanges();
}

Nulla di trascendentale… ma come molti di voi sapranno, se avete mai dato un occhio a NSK o vi siete mai cimentati nella realizzazione di una Unit of Work, quello che ci si aspetta è che il lavoro fatto dal metodo Commit() del data context sia come minimo transazionale.
In questo caso c’è però il rischio di incappare in un interessante problema, supponiamo che abbiate anche la seguente situazione:
VersioningEngineProcess vep = new VersioningEngineProcess();
Int32 revision = vep.GetRevision( first );

avete cioè un sistema che vi da una banale informazione di quante volte sia stata “salvata” una determinata entity, nella realtà dei fatti la cosa è molto più ostica ma qui ci basta il concetto, questa informazione è sia sul db e viene mantenuta in fase di Update/Insert ma è anche tracciata all’interno della entity stessa (nel proxy per essere precisi) in modo da evitare noiosi roundtrip sul db che non è detto che servano.
Quel versioning engine quindi si rivolge alla entity (al proxy) e le chiede la revision. Quello in cui rischiate di incappare è questo:
  • Apertura della transazione per il commit;
  • Verifica che la commit sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db;
  • Aggiornamento del valore di Revision;
  • Commit della transazione;
Se tutto va bene nessun problema…ma siccome siamo in un mondo difficile non è tutto oro quel che luccica e… potreste avere una condizione un filino più complessa:
IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();
first.FirstName = "Mauro";

first.Addresses[ 0 ].City = “Treviglio”;
In questo caso quello che succede è:
  • Apertura della transazione per il commit;
  • Verifica che la commit di IPerson sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db di IPerson;
  • Aggiornamento del valore di Revision di IPerson;
  • Verifica che la commit di IAddress sia possibile e non generi conflitti (Concorrenza Ottimistica, Versioning del Dato etc.. etc..);
  • Update fisico sul db di IAddress;
  • Aggiornamento del valore di Revision di IAddress;
  • Commit della transazione;
Se una delle operazioni fatte su IAddress fallisce otterremmo, giustamente un Rollback della transazione, ma incapperemmo nello spiacevole fatto che il valore di Revision memorizzatro nel proxy di IPerson non verrebbe “rolled back” e, in un’architettura articolata non è affatto operazione banale andare a farne il Rollback correttamente.
La soluzione c’è, è elegante, interessante e decisamente produttiva oltre ad incastrarsi perfettamente in un’architettura articolata: System.Transaction.IEnlistmentNotification, su cui ho scritto anche un articolo l’anno scorso per MSDN.
Una volta capito come funziona il modello transazionale/di enlistment di System.Transactions e in generale di una transazione il giochetto è abbastanza semplice, se poi abbiamo una architettura a servizi lo è ancora di più.
Nell’esempio di prima quello che succede, sulla Commit del DataContext, è più o meno questo:
using( TransactionScope tx = new TransactionScope( TransactionScopeOption.RequiresNew, options ) )
{
    actions.ForEach( action => action.Execute() );
    tx.Complete();
}

abbiamo una lista di azioni che devono essere eseguite, una di queste, quella per l’update di IPerson (idem quella di IAddress), fa una cosa del tipo:
ConflictDetectionEngineProcess cdep = new ConflictDetectionEngineProcess();
cdep.EnsureSafeUpdate( target, options );

PersistenceServiceProcess svc = new PersistenceServiceProcess();
svc.Update( target );

VersioningServiceProcess vsp = new VersioningServiceProcess();
vsp.MarkRevisioned( target );

Siccome siamo in una transazione è lecitissimo aspettarsi un rollback corretto, la prima operazione non scrive nulla fa solo alcuni test ed eventualmente scatena una exception, la seconda scrive fisicamente i dati su Sql Server, la terza aggiorna una “variabile” e qui casca l’asino perchè se dopo questa operazione abbiamo una exception non ne facciamo rollback.
Per fare in modo che il tutto sia implicito, automatico e trasparente è più che sufficiente implementare IEnlistmentNotification sul componente che si occupa di fare l’ultimo step:
sealed class ConcretePersonVersioningService : Service, IVersioningService, IEnlistmentNotification
{
    TransactionEnlistmentHelper transactionHelper = null;
    Boolean markRevisionedCalledInTransaction = false;
    PersonProxy entityCalledInTransaction;

    public void MarkRevisioned( IPerson entity )
    {
        this.CheckIsDisposed();

        /*
         * Passando true come parametro ensureTransaction non ci interessa più
         * verificare che siamo 'enlisted' perchè se non c'è una transazione
         * non arriveremmo mai qui ma otterremmo una eccezione.
         */
        transactionHelper = new TransactionEnlistmentHelper();
        transactionHelper.EnlistInTransaction( true, this, EnlistmentOptions.None );

        /* 
          * A questo punto siamo perfettamente consci del fatto che sappiamo
          * fare l'operazione e che non ci sarebbero exception, del resto è la set
          * di un field. ma siccome siamo in una Transazione ci limitiamo a tener
          * traccia del fatto che dobbiamo farla in fase di commit.
          * 
          * Se qualcosa non va è qui che dobbiamo notificarlo con una Exception
          * per fare in modo che ci sia un Rollback "gracefull".
          */        
        this.markRevisionedCalledInTransaction = true;
        this.entityCalledInTransaction = ( PersonProxy )entity;
    }

    void IEnlistmentNotification.Commit( Enlistment enlistment )
    {
        this.CheckIsDisposed();

        /*
         * Facciamo l'effetiva operazione, in questo modo
         * rispettiamo in pieno il principio ACID
         */
        if( this.markRevisionedCalledInTransaction && this.entityCalledInTransaction != null )
        {
            this.entityCalledInTransaction.Revision++;
            enlistment.Done();
        }
        else
        {
            throw new InvalidOperationException();
        }
    }

    void IEnlistmentNotification.InDoubt( Enlistment enlistment )
    {
        this.CheckIsDisposed();
        enlistment.Done();
    }

    void IEnlistmentNotification.Prepare( PreparingEnlistment preparingEnlistment )
    {
        this.CheckIsDisposed();

        if( this.markRevisionedCalledInTransaction && this.entityCalledInTransaction == null )
        {
            /*
             * Se per assurdo qualcosa fosse andato storto
             * siamo in grado di bloccare l'operazione
             */
            preparingEnlistment.ForceRollback();
        }
        else
        {
            preparingEnlistment.Prepared();
        }
    }

    void IEnlistmentNotification.Rollback( Enlistment enlistment )
    {
        this.CheckIsDisposed();

        /*
         * Rimettiamo a posto le cose come se
         * nulla fosse mai accaduto
         */
        this.markRevisionedCalledInTransaction = false;
        this.entityCalledInTransaction = null;
    }

    #endregion
}

Il tutto è abbastanza semplice: quando viene chiamato il metodo MarkRevisioned() ci rivolgiamo ad un TransactionEnlistmentHelper, che altro non è che un semplice warpper per facilitare la gestione delle transazioni, e chiediamo l’enlistment nella transazione corrente e poi aspettiamo che il tutto avvenga: Prepare() –> Commit() oppure Rollback().
Semplice, efficiace e decisamente bello da veder funzionare ;-)
.m