Update several entities atomically
Use IVoxTransactionRunner when a business operation must create, update or delete several entities as one unit.
Resolve the runner and the normal IVoxEntityUpdater from dependency injection:
using CommerceMind.Vox.Abstractions.Entities;
using CommerceMind.Vox.Abstractions.Transactions;
public class MoveProduct(
IVoxTransactionRunner transactions,
IVoxEntityUpdater updater)
{
public Task ExecuteAsync(
string productId,
long productVersion,
string oldCategoryId,
long oldCategoryVersion,
string newCategoryId,
long newCategoryVersion,
CancellationToken cancellationToken)
{
return transactions.ExecuteAsync(async token =>
{
await updater.UpdateAsync<Product>(
productId,
productVersion,
new ProductActions.MoveToCategory { CategoryId = newCategoryId },
cancellationToken: token);
await updater.UpdateAsync<Category>(
oldCategoryId,
oldCategoryVersion,
new CategoryActions.RemoveProduct { ProductId = productId },
cancellationToken: token);
await updater.UpdateAsync<Category>(
newCategoryId,
newCategoryVersion,
new CategoryActions.AddProduct { ProductId = productId },
cancellationToken: token);
}, cancellationToken);
}
}
The example action names are application code rather than built-in Vox actions.
Guarantees
Within the transaction:
- An exception, validation error or optimistic version conflict rolls back every entity change.
- Reads through Vox stores bypass optional caches and see changes made earlier in the transaction.
- The same entity may be changed several times; use the version returned by the previous action.
- Entity data, relations, history and Vox outbox records commit together.
- Cache synchronization, search indexing, calculations and entity events start after the commit.
- Nested calls to
ExecuteAsyncjoin the outer transaction and do not commit independently.
Do not run transaction operations concurrently with Task.WhenAll. A transaction is a sequential unit of work.
Vox does not automatically rerun the delegate after a conflict because application handlers are not assumed to be
safe to replay.
Storage boundary
Every entity read or written by the transaction must use the same authoritative storage provider. For example, a transaction can update several Entity Framework entity types, or several entity types in one authoritative Redis provider. It cannot combine an Entity Framework entity with a Redis-master entity.
Full-text search is unavailable inside a transaction because the external index cannot contain staged changes. Use the authoritative entity stores for transaction decisions.
Validation and events
Custom validators run before their entity change is persisted. They can read entities changed earlier in the transaction and can reject the complete transaction. Validators should check state rather than perform irreversible operations in external systems.
Action handlers may use IVoxEntityUpdater to make additional entity changes; those calls join the active
transaction.
Entity events and subscriptions are post-commit reactions. A rollback produces no events. Failures in an event consumer cannot roll back an already committed transaction, so external consumers should retain their existing idempotency and retry behavior.