{"record":{"id":"cb8d5093049259ad","repo":"elsa-workflows/elsa-core","slug":"the-elsa-user-was-deleted-while-its-external-identity-link-cb8d50","errorCode":null,"errorMessage":"The Elsa user was deleted while its external identity link was being replaced.","messagePattern":"The Elsa user was deleted while its external identity link was being replaced\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs","lineNumber":277,"sourceCode":"        if (await _userProvisioningService.ExistsAsync(user, wasCreated, cancellationToken))\n            return;\n\n        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);\n        await dbContext.ExternalIdentityLinks.Where(x => x.Id == link.Id).ExecuteDeleteAsync(cancellationToken);\n        throw new InvalidOperationException(\"The Elsa user was deleted while its external identity link was being created.\");\n    }\n\n    private async ValueTask EnsureReplacementUserStillExistsAsync(\n        PersistedExternalIdentityLink oldLink,\n        PersistedExternalIdentityLink replacementLink,\n        User replacementUser,\n        CancellationToken cancellationToken)\n    {\n        if (await _userProvisioningService.ExistsAsync(replacementUser, false, cancellationToken))\n            return;\n\n        await CompensateReplacementAsync(oldLink, replacementLink, cancellationToken);\n        throw new InvalidOperationException(\"The Elsa user was deleted while its external identity link was being replaced.\");\n    }\n\n    private async ValueTask CompensateReplacementAsync(\n        PersistedExternalIdentityLink oldLink,\n        PersistedExternalIdentityLink replacementLink,\n        CancellationToken cancellationToken)\n    {\n        var commitAttempted = false;\n        try\n        {\n            await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);\n            await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);\n            await dbContext.ExternalIdentityLinks.Where(x => x.Id == replacementLink.Id).ExecuteDeleteAsync(cancellationToken);\n            dbContext.ExternalIdentityLinks.Add(new PersistedExternalIdentityLink\n            {\n                Id = oldLink.Id,\n                TenantId = oldLink.TenantId,\n                ConnectionKey = oldLink.ConnectionKey,","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs#L259-L295","documentation":"Thrown by EnsureReplacementUserStillExistsAsync (called from ReplaceAsync). When replacing an existing external identity link with a new link/target user, the provisioner verifies the replacement user still exists; if it was deleted concurrently, it calls CompensateReplacementAsync to restore the previous link state and then throws this error. The invariant is: a link must never point at a non-existent user.","triggerScenarios":"ReplaceAsync writes the replacement link, then ExistsAsync(replacementUser) returns false because the replacement user row was deleted between creation and verification (concurrent admin delete, cleanup job, cascade).","commonSituations":"User deleted by an operator mid account-merge/re-link; a background job purging users races with an external-identity re-link operation; duplicate sign-in flows replacing each other's links.","solutions":["Retry the replace operation; after compensation the old link is restored and the replace can be attempted again safely.","Check CompensateReplacementAsync logs to confirm the previous link was actually restored before retrying; if this error's sibling messages appear, repair links manually.","Make user deletion flow through the provisioner so replacement targets are not deleted mid-operation.","Add row-level locking or an existence foreign key from link to user so deletes block/queue behind replacement."],"exampleFix":"// before\nvar link = await provisioner.ReplaceAsync(oldLink, replacement, cancellationToken);\n// after\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"deleted while its external identity link was being replaced\"))\n{\n    link = await provisioner.ReplaceAsync(oldLink, replacement, cancellationToken); // old link restored; retry\n}","handlingStrategy":"retry","validationCode":"bool replacementUserExists = await provisioningService.ExistsAsync(replacementUser, wasCreated: false, ct);\nif (!replacementUserExists) throw new InvalidOperationException(\"Replacement user missing before replace\");","typeGuard":null,"tryCatchPattern":"try { link = await provisioner.ReplaceAsync(oldLink, replacementLink, replacementUser, ct); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"deleted while its external identity link was being replaced\"))\n{\n    link = await provisioner.ReplaceAsync(oldLink, replacementLink, replacementUser, ct); // old link restored; safe retry\n}","preventionTips":["Prevent concurrent deletes of users involved in re-link operations","Retry the replace once; compensation guarantees consistent state first","Verify link state after compensation via LinkExistsAsync before retrying","Use locks/queueing per external subject for replace operations"],"tags":["race-condition","ef-core","user-provisioning","compensation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}