Skip to main content
Version: 2.4.1

Unit of Work

Overview​

The Unit of Work pattern coordinates writes to one or more repositories within a single transaction boundary. In RCommon the unit of work wraps a System.Transactions.TransactionScope, which means it can span multiple data stores without requiring distributed transaction infrastructure in most scenarios.

The typical workflow is:

  1. Create a unit of work via IUnitOfWorkFactory.
  2. Execute repository operations (add, update, delete) as normal.
  3. Call CommitAsync to commit; or dispose without committing to roll back.

RCommon also integrates with MediatR and Wolverine pipelines so that units of work can be opened and committed automatically around command handlers — see MediatR integration.

Installation​

NuGet Package
dotnet add package RCommon.Persistence

Configuration​

Configure the unit of work in your application startup using WithUnitOfWork<TBuilder>:

builder.Services.AddRCommon()
.WithUnitOfWork<DefaultUnitOfWorkBuilder>(unitOfWork =>
{
unitOfWork.SetOptions(options =>
{
options.AutoCompleteScope = true; // commit on dispose if not already committed
options.DefaultIsolation = IsolationLevel.ReadCommitted;
});
});

UnitOfWorkSettings defaults:

SettingDefaultDescription
DefaultIsolationReadCommittedThe isolation level for new TransactionScope instances
AutoCompleteScopefalseWhen true, the scope is completed on disposal if no explicit commit or rollback occurred

Usage​

Manual transaction management​

Inject IUnitOfWorkFactory and create a unit of work around your operations:

public class TransferService
{
private readonly IUnitOfWorkFactory _uowFactory;
private readonly IGraphRepository<Account> _accounts;

public TransferService(IUnitOfWorkFactory uowFactory, IGraphRepository<Account> accounts)
{
_uowFactory = uowFactory;
_accounts = accounts;
}

public async Task TransferAsync(Guid fromId, Guid toId, decimal amount,
CancellationToken cancellationToken)
{
using IUnitOfWork uow = _uowFactory.Create();

Account from = await _accounts.FindAsync(fromId, cancellationToken);
Account to = await _accounts.FindAsync(toId, cancellationToken);

from.Debit(amount);
to.Credit(amount);

await _accounts.UpdateAsync(from, cancellationToken);
await _accounts.UpdateAsync(to, cancellationToken);

await uow.CommitAsync(cancellationToken);
}
}

Disposing the IUnitOfWork without calling CommitAsync rolls back the transaction.

Specifying transaction mode and isolation level​

IUnitOfWorkFactory.Create has overloads for controlling how the unit of work participates in ambient transactions:

// Default settings from configuration
IUnitOfWork uow = _uowFactory.Create();

// Explicit transaction mode
IUnitOfWork uow = _uowFactory.Create(TransactionMode.New);

// Explicit mode and isolation level
IUnitOfWork uow = _uowFactory.Create(TransactionMode.New, IsolationLevel.Serializable);

TransactionMode maps to the TransactionScopeOption used when creating the TransactionScope:

ModeBehaviour
DefaultUses TransactionScopeOption.Required — joins an ambient transaction if one exists
NewUses TransactionScopeOption.RequiresNew — always creates a new transaction
SuppressUses TransactionScopeOption.Suppress — executes outside any ambient transaction

Unit of work lifecycle states​

IUnitOfWork.State reports the current lifecycle stage:

StateMeaning
CreatedNewly created; no commit or rollback has been attempted
CommitAttemptedCommitAsync has been called
CompletedSuccessfully committed
RolledBackThe transaction was rolled back
DisposedThe unit of work has been disposed and cannot be reused

Pipeline-based unit of work (MediatR)​

In the CleanWithCQRS example the unit of work is applied to every command handler automatically through the MediatR pipeline:

builder.Services.AddRCommon()
.WithMediator<MediatRBuilder>(mediator =>
{
mediator.AddUnitOfWorkToRequestPipeline(); // wraps each handler in a unit of work
})
.WithUnitOfWork<DefaultUnitOfWorkBuilder>(unitOfWork =>
{
unitOfWork.SetOptions(options =>
{
options.AutoCompleteScope = true;
options.DefaultIsolation = IsolationLevel.ReadCommitted;
});
});

With AutoCompleteScope = true the unit of work commits automatically when the pipeline behavior disposes it after a successful handler execution.

API Summary​

TypePurpose
IUnitOfWorkFactoryCreates IUnitOfWork instances with default or explicit transaction settings
IUnitOfWorkManages the transaction: exposes CommitAsync(), State, AutoComplete, IsolationLevel, TransactionMode, TransactionId
IUnitOfWorkBuilderFluent startup builder — call SetOptions(Action<UnitOfWorkSettings>) to configure defaults
UnitOfWorkSettingsHolds DefaultIsolation and AutoCompleteScope defaults
UnitOfWorkStateEnum: Created, CommitAttempted, Completed, RolledBack, Disposed
TransactionModeEnum: Default, New, Suppress
RCommonRCommon