Aplicarea tranzacțiilor în handlerul de Command
🎯 Obiectiv
Să înțelegem cum ne asigurăm că operațiile critice asupra datelor sunt consistente, adică fie se execută toate, fie niciuna (principiul atomicității).
🔹 De ce avem nevoie de tranzacții?
-
În aplicațiile reale, un Command poate modifica mai multe entități sau tabele.
-
Dacă o parte din operații reușesc și altele eșuează, baza de date rămâne într-o stare inconsistentă.
-
Soluția: rulăm totul într-o tranzacție, iar în caz de eroare facem rollback.
Exemplu: la crearea unui post nou pe blog, poate vrem să:
-
Adăugăm Post în tabelul Posts.
-
Adăugăm ContentText în tabelul ContentTexts.
-
Înregistrăm o intrare în AuditLog.
Dacă una dintre ele eșuează, toate trebuie anulate.
🔹 Varianta 1: Folosind IDbContextTransaction
În handlerul de Command putem lucra direct cu tranzacții EF Core:
public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
private readonly ApplicationDbContext _context;
public CreatePostCommandHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
{
using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
try
{
var post = new Post(request.Title, request.AuthorId);
_context.Posts.Add(post);
var content = new ContentText(post.Id, request.Content);
_context.ContentTexts.Add(content);
await _context.SaveChangesAsync(cancellationToken);
await _context.Database.CommitTransactionAsync(cancellationToken);
return post.Id;
}
catch
{
await _context.Database.RollbackTransactionAsync(cancellationToken);
throw;
}
}
}
🔹 Varianta 2: Folosind Unit of Work
Dacă vrem un layer mai curat, putem introduce un IUnitOfWork:
public interface IUnitOfWork
{
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
Implementarea (în ApplicationDbContext):
public class ApplicationDbContext : DbContext, IApplicationDbContext, IUnitOfWork
{
private IDbContextTransaction? _currentTransaction;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
public DbSet<Post> Posts => Set<Post>();
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
=> _currentTransaction = await Database.BeginTransactionAsync(cancellationToken);
public async Task CommitAsync(CancellationToken cancellationToken = default)
{
await SaveChangesAsync(cancellationToken);
await _currentTransaction?.CommitAsync(cancellationToken)!;
}
public async Task RollbackAsync(CancellationToken cancellationToken = default)
=> await _currentTransaction?.RollbackAsync(cancellationToken)!;
}
Apoi handlerul devine mai curat:
public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
private readonly IUnitOfWork _uow;
public CreatePostCommandHandler(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
{
await _uow.BeginTransactionAsync(cancellationToken);
try
{
var post = new Post(request.Title, request.AuthorId);
_uow.Posts.Add(post);
var content = new ContentText(post.Id, request.Content);
_uow.ContentTexts.Add(content);
await _uow.CommitAsync(cancellationToken);
return post.Id;
}
catch
{
await _uow.RollbackAsync(cancellationToken);
throw;
}
}
}
🔹 Când să alegi fiecare variantă?
-
IDbContextTransaction → bun pentru cazuri simple, tranzacții ocazionale.
-
UnitOfWork → bun pentru arhitecturi DDD mai complexe, unde vrei un contract explicit pentru tranzacții și mai multă testabilitate.