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

  1. Reload the document, merge your changes with its current state, and retry the save with the new actual version as expectedVersion.
  2. Catch DocumentStoreConcurrencyException and apply a bounded retry loop (re-read, re-apply, re-save).
  3. 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

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


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)