{"record":{"id":"c72446dd8e8ce1de","repo":"elsa-workflows/elsa-core","slug":"a-replacement-compensation-link-refers-to-a-deleted-user-and","errorCode":null,"errorMessage":"A replacement-compensation link refers to a deleted user and could not be removed. No credentials were issued.","messagePattern":"A replacement-compensation link refers to a deleted user and could not be removed\\. No credentials were issued\\.","errorType":"exception","errorClass":"AggregateException","httpStatus":null,"severity":"critical","filePath":"src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs","lineNumber":364,"sourceCode":"        return await dbContext.ExternalIdentityLinks.AsNoTracking().AnyAsync(x => x.Id == linkId, cancellationToken);\n    }\n\n    private async ValueTask RemoveReplacementLinksOrThrowAsync(\n        string oldLinkId,\n        string replacementLinkId,\n        Exception operationException,\n        CancellationToken cancellationToken)\n    {\n        try\n        {\n            await using var cleanupContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);\n            await cleanupContext.ExternalIdentityLinks\n                .Where(x => x.Id == replacementLinkId || x.Id == oldLinkId)\n                .ExecuteDeleteAsync(cancellationToken);\n        }\n        catch (Exception cleanupException)\n        {\n            throw new AggregateException(\n                \"A replacement-compensation link refers to a deleted user and could not be removed. No credentials were issued.\",\n                operationException,\n                cleanupException);\n        }\n    }\n\n    private async ValueTask RemoveStrandedUserAsync(User user, Exception linkException, CancellationToken cancellationToken)\n    {\n        try\n        {\n            await _userProvisioningService.RemoveAsync(user, cancellationToken);\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(exception, \"Could not remove the just-in-time user {UserId} after its external identity link failed\", user.Id);\n            throw new AggregateException(\n                \"External identity provisioning failed and its just-in-time user could not be removed. No credentials were issued.\",\n                linkException,","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs#L346-L382","documentation":"RemoveReplacementLinksOrThrowAsync is the last-resort cleanup when a replacement-compensation link references a deleted user. If even the multi-link ExecuteDeleteAsync fails, it throws an AggregateException combining the original operation exception and the cleanup exception, stating that no credentials were issued. Callers therefore never receive credentials while a link-to-deleted-user may exist.","triggerScenarios":"Compensation detects a link whose user is gone; the final cleanup ExecuteDeleteAsync over (oldLinkId, replacementLinkId) itself throws (connection failure, deadlock, timeout, cancellation), triggering the AggregateException path.","commonSituations":"Database outage during federated sign-in compensation; lock contention with cleanup jobs; misconfigured connection string causing repeated write failures during the same request.","solutions":["Check both inner exceptions to distinguish the original failure from the cleanup failure and fix the DB/connectivity root cause.","Manually delete ExternalIdentityLinks rows with the recorded link ids that reference deleted users.","Enable transient-fault retry (EnableRetryOnFailure) so cleanup deletes can succeed on retry.","Retry external sign-in after the database is reachable; provisioning will recreate consistent state."],"exampleFix":"// diagnosing\ntry { await provisioner.ReplaceAsync(...); }\ncatch (AggregateException agg) when (agg.Message.Contains(\"replacement-compensation link\"))\n{\n    foreach (var inner in agg.InnerExceptions) logger.LogError(inner, \"replace/cleanup failure\");\n}","handlingStrategy":"try-catch","validationCode":"bool orphan = !await userProvisioningService.ExistsAsync(user, false, ct)\n              && await LinkExistsAsync(linkId, ct); // link exists but user gone: needs cleanup","typeGuard":null,"tryCatchPattern":"try { await provisioner.ReplaceAsync(...); }\ncatch (AggregateException agg) when (agg.Message.Contains(\"replacement-compensation link\"))\n{\n    foreach (var inner in agg.InnerExceptions) logger.LogError(inner, \"Operation + cleanup both failed\");\n    // DB/link repair required before issuing any credentials\n}","preventionTips":["Inspect both InnerExceptions: the first is the original failure, the second the cleanup failure","Enable retrying execution strategies for transient DB faults","Manually purge link rows referencing deleted users after incidents","Ensure credentials are only issued after successful link persistence"],"tags":["aggregate-exception","ef-core","data-consistency","cleanup"],"backgroundTag":"database-write-failed","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"}