Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/masastack/MASA.Framework/llms.txt

Use this file to discover all available pages before exploring further.

The Unit of Work (UoW) pattern groups a set of database operations into a single atomic transaction and defers domain-event dispatch until the transaction succeeds. MASA Framework’s IUnitOfWork is the central coordinator between your EF Core DbContext, the IDomainEventBus, and the IEventBus pipeline — ensuring that domain events are only raised after data has been safely committed.

Installation

dotnet add package Masa.Contrib.Data.UoW.EFCore

IUnitOfWork Interface

public interface IUnitOfWork : IDisposable, IAsyncDisposable
{
    IServiceProvider ServiceProvider { get; }
    DbTransaction Transaction { get; }

    bool TransactionHasBegun { get; }
    bool? UseTransaction { get; set; }
    bool DisableRollbackOnFailure { get; set; }

    EntityState EntityState { get; set; }
    CommitState CommitState { get; set; }

    Task SaveChangesAsync(CancellationToken cancellationToken = default);
    Task CommitAsync(CancellationToken cancellationToken = default);
    Task RollbackAsync(CancellationToken cancellationToken = default);
}

Properties

PropertyTypeDescription
ServiceProviderIServiceProviderThe scoped service provider for this unit of work
TransactionDbTransactionThe active database transaction (throws if not begun)
TransactionHasBegunboolWhether a transaction is currently open
UseTransactionbool?Force on/off; null lets the provider decide per scenario
DisableRollbackOnFailureboolWhen true, skips automatic rollback on exception
EntityStateEntityStateUnChanged or Changed — whether EF has pending writes
CommitStateCommitStateUnCommited (open transaction) or Commited

Methods

MethodDescription
SaveChangesAsync()Persists tracked changes to the database via DbContext.SaveChangesAsync(). Also enqueues any pending domain events via IDomainEventBus.
CommitAsync()Commits the open transaction and calls IDomainEventBus.PublishQueueAsync() to dispatch all enqueued domain events.
RollbackAsync()Rolls back the transaction. Pending domain events are discarded with the transaction.

EntityState and CommitState

public enum EntityState
{
    UnChanged, // No tracked changes since last save
    Changed    // Pending changes exist
}

public enum CommitState
{
    UnCommited, // Transaction open and data has changed
    Commited    // No open transaction or data unchanged
}

ITransaction Interface

ITransaction is the marker interface that commands and events carry to propagate the IUnitOfWork through the handling pipeline.
public interface ITransaction
{
    [NotMapped]
    [JsonIgnore]
    IUnitOfWork? UnitOfWork { get; set; }
}
IDomainEvent, DomainCommand, and the CQRS Command type all implement ITransaction, allowing the TransactionEventMiddleware to coordinate transactions automatically.

Registration

builder.Services
    .AddEventBus()
    .AddMasaDbContext<ShopDbContext>(opts =>
        opts.UseSqlServer(
            builder.Configuration.GetConnectionString("DefaultConnection"))
            .UseUoW());         // registers IUnitOfWork backed by ShopDbContext

Via EventBus builder

builder.Services
    .AddEventBus(eventBusBuilder =>
        eventBusBuilder.UseUoW<ShopDbContext>(opts =>
            opts.UseSqlServer(connStr)));

TransactionEventMiddleware

When UseUoW<TDbContext>() is configured, the EventBus automatically inserts TransactionEventMiddleware<TEvent> around every event handler. This middleware:
  1. Sets UseTransaction = true on the IUnitOfWork if not already configured
  2. Awaits the handler pipeline (next())
  3. Calls SaveChangesAsync() then CommitAsync() on success
  4. Calls RollbackAsync() on any exception (unless DisableRollbackOnFailure = true)
This means you almost never need to interact with IUnitOfWork directly in command handlers. The middleware handles the full save-and-commit lifecycle.
// TransactionEventMiddleware logic (simplified from source)
public override async Task HandleAsync(TEvent @event, EventHandlerDelegate next)
{
    try
    {
        if (_unitOfWork is { UseTransaction: null })
            _unitOfWork.UseTransaction = true;

        await next();

        if (_unitOfWork != null)
        {
            await _unitOfWork.SaveChangesAsync();
            await _unitOfWork.CommitAsync();
        }
    }
    catch (Exception)
    {
        if (_unitOfWork is { DisableRollbackOnFailure: false })
            await _unitOfWork!.RollbackAsync();
        throw;
    }
}

Usage in Command Handlers

The typical pattern with CQRS — the UoW is managed entirely by the middleware:
builder.Services
    .AddEventBus()
    .AddMasaDbContext<ShopDbContext>(opts =>
        opts.UseSqlServer(connStr).UseUoW());

public class OrderCommandHandler
{
    private readonly IOrderRepository _repo;

    public OrderCommandHandler(IOrderRepository repo) => _repo = repo;

    [EventHandler]
    public async Task CreateOrderAsync(CreateOrderCommand command)
    {
        var order = new Order(Guid.NewGuid(), command.CustomerName);

        foreach (var item in command.Items)
            order.AddItem(new OrderItem(item.ProductId, item.Quantity));

        await _repo.AddAsync(order);
        // TransactionEventMiddleware calls SaveChangesAsync + CommitAsync
        // after this method returns. Domain events are dispatched on CommitAsync.
    }
}

Manual UoW Control

For scenarios outside the EventBus pipeline — such as background jobs or scripts — inject IUnitOfWork directly:
public class OrderBatchService
{
    private readonly IOrderRepository _repo;
    private readonly IUnitOfWork _uow;

    public OrderBatchService(IOrderRepository repo, IUnitOfWork uow)
    {
        _repo = repo;
        _uow = uow;
    }

    public async Task ImportOrdersAsync(IEnumerable<OrderImportDto> imports)
    {
        try
        {
            foreach (var dto in imports)
            {
                var order = new Order(Guid.NewGuid(), dto.CustomerName);
                await _repo.AddAsync(order);
            }

            await _uow.SaveChangesAsync();
            await _uow.CommitAsync();
        }
        catch
        {
            await _uow.RollbackAsync();
            throw;
        }
    }
}

Transaction Options

Control transaction behavior when registering the UoW:
builder.Services.AddMasaDbContext<ShopDbContext>(opts =>
    opts.UseSqlServer(connStr)
        .UseUoW(
            disableRollbackOnFailure: false, // default: auto-rollback on exception
            useTransaction: true             // default: null (provider decides)
        ));
Setting useTransaction: false is useful for read-heavy services or tests where transactional overhead is undesirable. Domain events are still dispatched via CommitAsync, but no database transaction is opened.

Build docs developers (and LLMs) love