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 '{actual?.Version?.ToString() ?? "<none>"}'. What it means
This is the failure detail thrown after an optimistic-concurrency update fails at the MongoDB level (e.g. a FindOneAndUpdate/ReplaceOne with a version filter matched nothing). ThrowConcurrencyExceptionAsync reloads the document to report its actual version (or <none> if it was deleted) inside DocumentStoreConcurrencyException, giving you the expected vs actual versions.
Solutions
- Reload the document, merge your changes with its current state, and retry the save with the new actual version as expectedVersion.
- Catch DocumentStoreConcurrencyException and apply a bounded retry loop (re-read, re-apply, re-save).
- If the message shows actual version '<none>', the document was deleted — decide whether to recreate it or skip the write.
Example fix
// before
await store.SaveAsync(collection, request with { ExpectedVersion = 7 }, ct); // throws: actual was 8
// after
try {
await store.SaveAsync(collection, request with { ExpectedVersion = 7 }, ct);
} catch (DocumentStoreConcurrencyException) {
var current = await store.LoadAsync(collection, request.Id, ct);
await store.SaveAsync(collection, request with { ExpectedVersion = current?.Version }, ct);
} Defensive patterns
Strategy: retry
Try / catch
for (var attempt = 0; attempt < 3; attempt++)
{
try
{
await store.SaveAsync(collection, request, ct);
break;
}
catch (DocumentStoreConcurrencyException) when (attempt < 2)
{
var current = await store.LoadAsync(collection, request.Id, ct);
request = request with { ExpectedVersion = current?.Version };
}
} Prevention
- Treat this exception as a normal race outcome: catch it, reload, and retry rather than failing the operation.
- If the message reports actual version '<none>', handle the deleted-document case explicitly.
- Serialize writes for the same document through a single logical owner (e.g. per-document lock) in high-contention scenarios.
When it happens
Trigger: SaveAsync or DeleteAsync attempted a version-guarded write whose filter matched no documents — another writer changed or deleted the document between your read and write, or the document never existed at the expected version.
Common situations: Race between two concurrent saves where the second overwrite is rejected; a delete racing a save; retrying a queued operation after the document was already updated or removed.
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/316a0f9661132d87.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Persistence.VNext.MongoDb/MongoDbDocumentStore.cs:206
private static FilterDefinition<BsonDocument> CreateDeleteFilter(string id, long? expectedVersion)
{
var filter = Builders<BsonDocument>.Filter.Eq("_id", id);
return expectedVersion is null ? filter : Builders<BsonDocument>.Filter.And(filter, Builders<BsonDocument>.Filter.Eq("Version", expectedVersion.Value));
}
private static bool DidExpectedWriteSucceed(ReplaceOneResult result)
{
if (!result.IsAcknowledged)
return true;
return result.MatchedCount > 0;
}
private static async Task ThrowConcurrencyExceptionAsync(IMongoCollection<BsonDocument> collection, string storageUnit, string documentId, long expectedVersion, CancellationToken cancellationToken)
{
var actual = await LoadDocumentAsync(collection, storageUnit, documentId, cancellationToken);
throw new DocumentStoreConcurrencyException(storageUnit, documentId, expectedVersion, actual?.Version);
}
private static BsonDocument CreateDocument(StoredDocument document, IReadOnlyDictionary<string, string?> indexValues)
{
return new BsonDocument
{
["_id"] = document.Id,
["Content"] = document.Content,
["Version"] = document.Version,
["CreatedAt"] = new BsonDateTime(document.CreatedAt.UtcDateTime),
["UpdatedAt"] = new BsonDateTime(document.UpdatedAt.UtcDateTime),
["Data"] = ParseContent(document.Content),
["IndexValues"] = new BsonDocument(indexValues.Select(x => new BsonElement(x.Key, x.Value is null ? BsonNull.Value : BsonValue.Create(x.Value))))
};
}
private static BsonValue ParseContent(string content)
{View on GitHub (pinned to fe9217bdfa)