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.

Scrie un comentariu

Adresa de mail nu va fi publicata. Campurile obligatorii sunt marcate cu *