Aplicarea tranzacțiilor în handlerul de Command
Doru Bulubasa
29 August 2025

🎯 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ă:

  1. Adăugăm Post în tabelul Posts.

  2. Adăugăm ContentText în tabelul ContentTexts.

  3. Î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.