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

  1. Re-read the document to get its current Version, apply your changes, and retry with the fresh expectedVersion.
  2. Catch DocumentStoreConcurrencyException and implement a retry-with-reload loop for idempotent writes.
  3. 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

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


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)