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

  1. Re-load the document to get its current version, merge your changes, and retry the save with the fresh expectedVersion.
  2. If you truly intend a blind overwrite/delete, omit expectedVersion (pass null) to skip the check — only when last-writer-wins is acceptable.
  3. For deletes, distinguish between 'already deleted by another node' (version 0 actual) and a genuine conflict and handle each case.
  4. 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

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


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)