elsa-workflows/elsa-core · error · DocumentStoreConcurrencyException
Document ' ' in storage unit ' ' expected version ' ' but…
Error message
Document '{documentId}' in storage unit '{storageUnit}' expected version '{expectedVersion?.ToString() ?? "<none>"}' but actual version was '{actualVersion?.ToString() ?? "<none>"}'. What it means
ValidateExpectedVersion implements optimistic concurrency: when a request carries an expectedVersion, the store compares it against the document's current version (treating a missing document as version 0) and throws DocumentStoreConcurrencyException on mismatch. This prevents lost updates when concurrent writers modify the same document.
Solutions
- Re-read the document to get its current Version, apply your changes, and retry with the fresh expectedVersion.
- Catch DocumentStoreConcurrencyException and implement a retry-with-reload loop for idempotent writes.
- If you don't need concurrency control, pass expectedVersion: null, but understand you lose optimistic-locking protection.
Example fix
// before
await store.SaveAsync(collection, request with { ExpectedVersion = 3 }, ct); // actual is 4
// after
var existing = await store.LoadAsync(collection, id, ct);
await store.SaveAsync(collection, request with { ExpectedVersion = existing?.Version }, ct); Defensive patterns
Strategy: retry
Validate before calling
var current = await store.LoadAsync(collection, id, ct);
if (expectedVersion.HasValue && expectedVersion.Value != (current?.Version ?? 0))
throw new InvalidOperationException("Version conflict detected before save; reload first."); Try / catch
try
{
await store.SaveAsync(collection, request, ct);
}
catch (DocumentStoreConcurrencyException ex)
{
logger.LogWarning(ex, "Concurrency conflict on {Unit}/{Id}", ex.StorageUnit, ex.DocumentId);
// reload and retry
} Prevention
- Always pass the version obtained from your last read as expectedVersion; never hard-code versions.
- Keep read-modify-write windows short to reduce conflict probability.
- Implement a bounded retry (reload, re-apply, re-save) around concurrency-sensitive writes.
When it happens
Trigger: Calling SaveAsync or DeleteAsync with a non-null expectedVersion that does not match the stored document's Version (or the document does not exist while expectedVersion != 0).
Common situations: Two processes/workflow instances writing the same document concurrently; a client retrying an operation after another writer already incremented the version; stale cached version values after a delete+recreate.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Document ' ' in storage unit ' ' expected version ' ' but…
- Document ' ' in storage unit ' ' expected version ' ' but…
- Storage unit ' ' is not declared in the persistence schema.
- Document ID is required.
- Storage unit ' ' requires index values for fields ' '.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/b3fa4b2ae64a0f67.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Persistence.VNext.MongoDb/MongoDbDocumentStore.cs:170
var missingFields = collection.Indexes
.SelectMany(x => x.Fields)
.Distinct(StringComparer.Ordinal)
.Where(field => !request.IndexValues.ContainsKey(field))
.Order(StringComparer.Ordinal)
.ToList();
if (missingFields.Count > 0)
throw new DocumentStoreValidationException($"Storage unit '{collection.Name}' requires index values for fields '{string.Join(", ", missingFields)}'.");
}
private static void ValidateExpectedVersion(string storageUnit, string documentId, long? expectedVersion, long? actualVersion)
{
if (expectedVersion is null)
return;
if (expectedVersion != (actualVersion ?? 0))
throw new DocumentStoreConcurrencyException(storageUnit, documentId, expectedVersion, actualVersion);
}
private static async Task<StoredDocument?> LoadDocumentAsync(IMongoCollection<BsonDocument> collection, string storageUnit, string id, CancellationToken cancellationToken)
{
var document = await collection.Find(Builders<BsonDocument>.Filter.Eq("_id", id)).FirstOrDefaultAsync(cancellationToken);
return document is null ? null : ReadDocument(document, storageUnit);
}
private static FilterDefinition<BsonDocument> CreateSaveFilter(SaveDocumentRequest request)
{
var filter = Builders<BsonDocument>.Filter;
if (request.ExpectedVersion is null)
return filter.Eq("_id", request.Id);
return filter.And(filter.Eq("_id", request.Id), filter.Eq("Version", request.ExpectedVersion.Value));
}
View on GitHub (pinned to fe9217bdfa)