Transactions

In modern software development, especially when dealing with relational databases, ensuring data integrity and consistency is critical. This is where the concept of transactions comes into play. A transaction is a sequence of operations performed as a single logical unit of work—either all changes are successfully committed, or none are applied if any step fails.


What is a Transaction?

A transaction follows the ACID principles:

  • Atomicity: All steps succeed or none do. Partial updates never happen.

  • Consistency: The database always moves from one valid state to another.

  • Isolation: Concurrent transactions do not interfere with each other’s data.

  • Durability: Once a transaction is committed, changes are permanent—even after a system crash.


Why Use Transactions in EF Core?

Transactions are essential for scenarios where you have multiple database operations that must either succeed together or fail together. Common examples include:

  • Order processing: Deducting inventory and charging a customer must both succeed, or neither should.

  • Error handling: If any part of a multi-step process fails, all previous changes can be rolled back, leaving your data consistent.

  • Concurrent updates: Preventing data corruption when multiple processes are updating related records.


Transactions in REN.Kit with EF Core

Entity Framework Core (EF Core) provides built-in support for transactions, and REN.Kit’s Unit of Work implementation makes this process even easier.

There are two primary ways to work with transactions:

  • Implicit Transactions: Every SaveChanges (or SaveChangesAsync) call is automatically wrapped in a transaction by EF Core.

  • Explicit Transactions: You manually begin, commit, or roll back transactions. This is useful for more advanced workflows spanning multiple save operations or involving multiple repositories.


The REN.Kit Way

The RENUnitOfWork class provides smart transaction handling:

 public virtual void SaveChanges(bool useTransaction = false)
 {
     EnsureNotDisposed();

     if (IsInTransaction)
     {
         _context.SaveChanges();
         return;
     }

     if (!useTransaction)
     {
         _context.SaveChanges();
         return;
     }

     using var transaction = _context.Database.BeginTransaction();
     try
     {
         _context.SaveChanges();
         transaction.Commit();
     }
     catch
     {
         transaction.Rollback();
         throw;
     }
 }

REN.Kit’s SaveChanges(bool useTransaction = false) method provides flexible transaction control for every scenario:

  • If a transaction is already active (for example, after calling BeginTransaction()), SaveChanges() will not create a new transaction. All changes are simply committed within the existing transaction.

    This allows you to manually control complex workflows spanning multiple operations.

  • If no transaction is active:

    • If useTransaction is false (the default), REN.Kit just calls _context.SaveChanges(), saving changes directly—ideal for simple, one-off updates.

    • If useTransaction is true, REN.Kit will automatically create a transaction for you, apply the changes inside it, and then commit (or roll back on error).

      Perfect for atomic operations without manual transaction management!

Why was it designed this way?

  • Maximum Flexibility: You get full control for multi-step workflows, but can also enjoy auto-transactions for simpler use cases.

  • Performance: No unnecessary transactions—small updates stay fast and lightweight.

  • Explicit Developer Control: If you want, you can always start and manage your own transaction for advanced scenarios.


Example: Using Unit of Work and Transactions Together

Let’s see a practical example using REN.Kit’s Unit of Work, repositories, and transactions together:

[Route("api/[controller]")]
[ApiController]
public class OrdersController(IRENUnitOfWork<AppDbContext> uow) : ControllerBase
{
    private readonly IRENRepository<Order> _orderRepository = uow.GetRepository<Order>();
    private readonly IRENRepository<OrderItem> _itemRepository = uow.GetRepository<OrderItem>();

    [HttpPost("place-order")]
    public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderRequest request)
    {
        uow.BeginTransaction();

        try
        {
            // Create the order
            var order = new Order { ... };
            await _orderRepository.InsertAsync(order);

            // Add order items
            var items = ... // map from request
            await _itemRepository.InsertAsync(items);

            await uow.SaveChangesAsync();
            await uow.CommitTransactionAsync();

            return Ok(new { order.Id });
        }
        catch
        {
            await uow.RollbackTransactionAsync();
            return BadRequest();
        }
    }
}

In this example, all database operations are grouped into a single transaction. If anything fails, all changes are rolled back automatically—keeping your data safe and consistent.

Last updated