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
Optimistic concurrency check: when a save or delete passes an expectedVersion, the store compares it to the document's actual stored version (missing documents count as version 0). A mismatch throws DocumentStoreConcurrencyException instead of silently overwriting or deleting data changed by someone else.
Solutions
- Re-load the document to get its current version, merge your changes, and retry the save with the fresh expectedVersion.
- If you truly intend a blind overwrite/delete, omit expectedVersion (pass null) to skip the check — only when last-writer-wins is acceptable.
- For deletes, distinguish between 'already deleted by another node' (version 0 actual) and a genuine conflict and handle each case.
- Wrap the operation in a retry loop that reloads and re-applies on DocumentStoreConcurrencyException, bounded by attempts.
Example fix
// before await store.DeleteAsync(unit, id, expectedVersion: loadedDoc.Version); // someone else updated it meanwhile // after var fresh = await store.LoadAsync(unit, id); if (fresh is null) return; // already deleted await store.DeleteAsync(unit, id, expectedVersion: fresh.Version);
Defensive patterns
Strategy: retry
Validate before calling
var current = await store.LoadAsync(unit, id); if (current?.Version != expectedVersion) throw new ConcurrencyRetryRequiredException();
Try / catch
for (var attempt = 0; attempt < 3; attempt++)
{
try { await store.SaveAsync(collection, request); return; }
catch (DocumentStoreConcurrencyException) { request = await ReloadAndMergeAsync(request); }
}
throw new InvalidOperationException("Concurrent update conflict persisted after retries."); Prevention
- Always re-read the document immediately before an expected-version write.
- Only pass expectedVersion when optimistic concurrency is intended; omit for last-writer-wins.
- Treat version 0 actual on delete as 'already gone' and succeed idempotently.
When it happens
Trigger: SaveAsync or DeleteAsync with expectedVersion set while another writer already saved (bumped) the version, or the document was created/deleted between your read and write.
Common situations: Two concurrent workflow instances or API requests updating the same document; retrying a save after a timeout without re-reading the current version; stale cached copy used as the basis for a delete.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- Document ' ' in storage unit ' ' expected version ' ' but…
- Document ' ' in storage unit ' ' expected version ' ' but…
- A secret named ' ' already exists.
- Failed to insert AI conversation
- Failed to insert AI proposal
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/f00901b840bd7018.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Persistence.VNext.Relational/Documents/RelationalDocumentStore.cs:138
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 async Task OpenAsync(CancellationToken cancellationToken)
{
if (_connection.State != ConnectionState.Open)
await _connection.OpenAsync(cancellationToken);
}
private async Task<StoredDocument?> LoadAsync(DbTransaction? transaction, string storageUnit, string id, CancellationToken cancellationToken)
{
await using var command = _connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = _dialect.RenderSelectDocumentSql();
AddParameter(command, "storageUnit", storageUnit);
AddParameter(command, "id", id);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))View on GitHub (pinned to fe9217bdfa)