System.Transactions.IEnlistmentNotification: interessante… decisamente!
IChangeTrackingServiceContainer container = ChangeTrackingServiceContainer.GetProvider();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.
IChangeTrackingService tracking = container.CreateTrackingService();
DataContextProcess dataContext = new DataContextProcess();
IEntityCollectionallPersons = dataContext.GetAll ();
IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();
first.FirstName = "Mauro";
if( tracking.IsChanged )
{
IEnumerableadvisory = tracking.GetAdvisory();
dataContext.Commit( advisory );
tracking.AcceptChanges();
}
In questo caso c’è però il rischio di incappare in un interessante problema, supponiamo che abbiate anche la seguente situazione:
VersioningEngineProcessavete 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.vep = new VersioningEngineProcess ();
Int32 revision = vep.GetRevision( first );
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;
IPerson first = allPersons.Where( p => p.LastName == "Servienti" ).First();In questo caso quello che succede è:
first.FirstName = "Mauro";
first.Addresses[ 0 ].City = “Treviglio”;
- 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;
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 ) )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:
{
actions.ForEach( action => action.Execute() );
tx.Complete();
}
ConflictDetectionEngineProcessSiccome 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.cdep = new ConflictDetectionEngineProcess ();
cdep.EnsureSafeUpdate( target, options );
PersistenceServiceProcesssvc = new PersistenceServiceProcess ();
svc.Update( target );
VersioningServiceProcessvsp = new VersioningServiceProcess ();
vsp.MarkRevisioned( target );
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, IVersioningServiceIl 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()., 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
}
Semplice, efficiace e decisamente bello da veder funzionare ;-)
.m