{"record":{"id":"f00901b840bd7018","repo":"elsa-workflows/elsa-core","slug":"document-documentid-in-storage-unit-storageunit-expected-f00901","errorCode":null,"errorMessage":"Document '{documentId}' in storage unit '{storageUnit}' expected version '{expectedVersion?.ToString() ?? \"<none>\"}' but actual version was '{actualVersion?.ToString() ?? \"<none>\"}'.","messagePattern":"Document '(.+?)' in storage unit '(.+?)' expected version '(.+?)' but actual version was '(.+?)'\\.","errorType":"exception","errorClass":"DocumentStoreConcurrencyException","httpStatus":409,"severity":"error","filePath":"src/modules/Elsa.Persistence.VNext.Relational/Documents/RelationalDocumentStore.cs","lineNumber":138,"sourceCode":"\n        var missingFields = collection.Indexes\n            .SelectMany(x => x.Fields)\n            .Distinct(StringComparer.Ordinal)\n            .Where(field => !request.IndexValues.ContainsKey(field))\n            .Order(StringComparer.Ordinal)\n            .ToList();\n\n        if (missingFields.Count > 0)\n            throw new DocumentStoreValidationException($\"Storage unit '{collection.Name}' requires index values for fields '{string.Join(\", \", missingFields)}'.\");\n    }\n\n    private static void ValidateExpectedVersion(string storageUnit, string documentId, long? expectedVersion, long? actualVersion)\n    {\n        if (expectedVersion is null)\n            return;\n\n        if (expectedVersion != (actualVersion ?? 0))\n            throw new DocumentStoreConcurrencyException(storageUnit, documentId, expectedVersion, actualVersion);\n    }\n\n    private async Task OpenAsync(CancellationToken cancellationToken)\n    {\n        if (_connection.State != ConnectionState.Open)\n            await _connection.OpenAsync(cancellationToken);\n    }\n\n    private async Task<StoredDocument?> LoadAsync(DbTransaction? transaction, string storageUnit, string id, CancellationToken cancellationToken)\n    {\n        await using var command = _connection.CreateCommand();\n        command.Transaction = transaction;\n        command.CommandText = _dialect.RenderSelectDocumentSql();\n        AddParameter(command, \"storageUnit\", storageUnit);\n        AddParameter(command, \"id\", id);\n\n        await using var reader = await command.ExecuteReaderAsync(cancellationToken);\n        if (!await reader.ReadAsync(cancellationToken))","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Persistence.VNext.Relational/Documents/RelationalDocumentStore.cs#L120-L156","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait store.DeleteAsync(unit, id, expectedVersion: loadedDoc.Version); // someone else updated it meanwhile\n// after\nvar fresh = await store.LoadAsync(unit, id);\nif (fresh is null) return; // already deleted\nawait store.DeleteAsync(unit, id, expectedVersion: fresh.Version);","handlingStrategy":"retry","validationCode":"var current = await store.LoadAsync(unit, id);\nif (current?.Version != expectedVersion) throw new ConcurrencyRetryRequiredException();","typeGuard":null,"tryCatchPattern":"for (var attempt = 0; attempt < 3; attempt++)\n{\n    try { await store.SaveAsync(collection, request); return; }\n    catch (DocumentStoreConcurrencyException) { request = await ReloadAndMergeAsync(request); }\n}\nthrow new InvalidOperationException(\"Concurrent update conflict persisted after retries.\");","preventionTips":["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."],"tags":["persistence","concurrency","optimistic-locking"],"backgroundTag":"conflicting-config-options","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}