Mai întâi, desigur, trebuie să stabilim coordonatele discuției. Este vorba despre situația în care lucrăm (fie pentru că am ales ca atare, fie că nu avem de-ales) la un proiect unde accesul la date este abstractizat:
– printr-un model de date implementat printr-un set de clase (entități);
– folosind un ORM oarecare pentru gestionarea corespondenței dintre entități și diversele artefacte din baza de date (tabele, dar nu numai);
– folosind repository-uri pentru a accesa entitățile respective;
– în sfârșit, ca să adăugăm o dimensiune concretă, folosind C# ca limbaj de bază și Entity Framework drept ORM.
Folosind Entity Framework, se pune problema gestionării DbContext-ului, adică a modului în care trebuie distribuit între diversele repository-uri implicate într-o singură tranzacție. Dintre toate variantele, m-am oprit în cele din urmă pe cele descrise aici (și vă recomand să citiți înainte de a continua).
Astfel, să preluăm același UserService exemplificat în articol drept punct de plecare, cu aceeași metodă – MarkUserAsPremium (pastrând inclusiv implementarea repository-ului, pe care nu o s-o mai reiau, pentru că nu aceea contează în cele ce urmează).
class UserService : IUserService
{
private IUserRepository _userRepository;
private IDbContextScopeFactory _dbContextScopeFactory;
...
public void MarkUserAsPremium(Guid userId)
{
using (IDbContextScope dbContextScope
= _dbContextScopeFactory.Create())
{
User user = _userRepository.Get(userId);
user.IsPremiumUser = true;
dbContextScope.SaveChanges();
}
}
}
Ideea este că perechea IDbContextScope/IDbContextScopeFactory, constituie, în cazul de față, un detaliu al implementării repository-ului, care folosește și, eventual, depinde de existența unui IDbContextScope ambiental. Dar serviciul IUserService lucrează, în definitiv, cu abstractizări: entități, repository-uri etc.
Așdar, în definitiv, ce reprezintă, IDbContextScope? Ce-i mâna pe ei în luptă? Ce-au dorit, acel apus? Exact ce-i zice numele, definește în același timp granița unei tranzacții abstracte de business, dar și limita tranzacției corespunzătoare în relație cu baza de date. Și mai precis, reprezintă modul în care o tranzacției de business este tradusă-ntr-una cu baza de date.
Ținând cont de toate acestea, ar fi bine, ca-n oricare alt aspect al vieții, să exprimăm explicit ce face fiecare lucru să fie ceea ce este și nu altceva, dar nici prea multe informații. Adică să clarificăm sensul și să nu legăm restul bazei de cod sursă de un aspect de implementare relativ minor cantitativ.
Primul pas este să definim două interfețe pentru fiecare din cele două elemente cu care interacționăm:
– IBusinessTransaction pentru a evidenția sensul IDbContextScope;
– respectiv IBusinessTransactionCoordinator pentru a evidenția sensul IDbContextScopeFactory.
public interface IBusinessTransactionCoordinator
{
IBusinessTransaction BeginTransaction();
}
public interface IBusinessTransaction : IDisposable
{
void Commit();
Task CommitAsync();
void Rollback();
Task RollbackAsync();
}
Implementarea, după cum vă așteptați, este foarte la obiect. In primul rând, cea pentru IBusinessTransaction este doar o fațadă peste un IDbContextScope, deci nu este responsabilă pentru crearea acestuia, ci doar de manipularea corectă:
public class DbContextScopeBusinessTransaction
: IBusinessTransaction
{
private bool mIsDisposed = false;
private IDbContextScope mDbContextScope;
public DbContextScopeBusinessTransaction( IDbContextScope scope )
{
mDbContextScope = scope
?? throw new ArgumentNullException( nameof( scope ) );
}
public void Commit()
{
mDbContextScope.SaveChanges();
}
public async Task CommitAsync()
{
await mDbContextScope.SaveChangesAsync();
}
public void Rollback()
{
Dispose();
}
public Task RollbackAsync()
{
Dispose();
return Task.CompletedTask;
}
protected void Dispose( bool disposing )
{
if ( !mIsDisposed )
{
if ( disposing )
{
mDbContextScope.Dispose();
mDbContextScope = null;
}
mIsDisposed = true;
}
}
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( this );
}
}
Iar cea pentru IBusinessTransactionCoordinator doar creează un DbContextScopeBusinessTransaction, folosind un IDbContextScopeFactory pentru instanțierea unui obiect de tip IDbContextScope. Bănuiesc că poate prelua și alte răspunderi de la IDbContextScopeFactory, însă momentan, pentru exemplificare, este suficient.
public class DbContextScopeBusinessTransactionCoordinator
: IBusinessTransactionCoordinator
{
private IDbContextScopeFactory mDbContextScopeFactory;
public DbContextScopeBusinessTransactionCoordinator(
IDbContextScopeFactory scopeFactory
)
{
mDbContextScopeFactory = scopeFactory
?? throw new ArgumentNullException( nameof( scopeFactory ) );
}
public IBusinessTransaction BeginTransaction()
{
IDbContextScope scope = mDbContextScopeFactory.Create();
return new DbContextScopeBusinessTransaction( scope );
}
}
Utilizarea este și mai oablă. De fapt, se folosesc la fel ca perechea peste care au fost dezvoltate conceptele (în definitiv, scopul a fost clarificarea a ceea ce se-ntâmplă, nu îngreunarea). Iată și codul inițial rescris:
class UserService : IUserService
{
private IUserRepository _userRepository;
private IBusinessTransactionCoordinator _txCoordinator;
...
public void MarkUserAsPremium(Guid userId)
{
using (IBusinessTransaction tx
= _txCoordinator.Create())
{
User user = _userRepository.Get(userId);
user.IsPremiumUser = true;
tx.Commit();
}
}
}