elsa-workflows/elsa-core · error · InvalidOperationException

The restored previous link was removed after its user was…

Error message

The restored previous link was removed after its user was deleted, but its first cleanup attempt failed.

What it means

Thrown in CompensateReplacementAsync's cleanup phase. After the restored previous link is detected as removed (its user was also deleted), the code attempts a final ExecuteDeleteAsync of the old link; if that delete throws, RemoveReplacementLinksOrThrowAsync runs and this InvalidOperationException is thrown with the cleanup exception attached, ensuring no orphan link to a deleted user remains.

Solutions

  1. Retry the sign-in/replace operation once the database is healthy; compensation will re-run from the current link state.
  2. Check the inner cleanupException for transient errors and configure the DbContext/retry strategy (EnableRetryOnFailure) for transient faults.
  3. Inspect ExternalIdentityLinks for leftover old/replacement link rows and delete any pointing to deleted users.
  4. Reduce contention between user-cleanup jobs and sign-in provisioning to avoid deadlocks on link rows.

Example fix

// after
optionsBuilder.UseSqlServer(cs, o => o.EnableRetryOnFailure()); // survive transient cleanup failures
Defensive patterns

Strategy: retry

Try / catch

try { await provisioner.ReplaceAsync(...); }
catch (InvalidOperationException ex) when (ex.Message.Contains("first cleanup attempt failed"))
{
    logger.LogError(ex.InnerException, "Cleanup delete failed transiently; safe to retry after DB recovers");
    await Task.Delay(TimeSpan.FromSeconds(2), ct);
    await provisioner.ReplaceAsync(...); // retry
}

Prevention

When it happens

Trigger: The restored old link's user is deleted (link auto-removed or detectably gone) and the compensating ExecuteDeleteAsync on ExternalIdentityLinks fails — DB timeout, deadlock, connection failure, or cancellation during cleanup.

Common situations: Transient SQL outages during sign-in; deadlock with a concurrent cleanup job deleting the same link row; command cancellation when the HTTP request aborts mid-compensation.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/18da60563bccab69. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/EFCoreExternalIdentityProvisioner.cs:336

                    "The replacement link was removed after its target user was deleted, but the previous link could not be restored.",
                    compensationException);
            }
        }

        // An indeterminate user-directory failure must not be mistaken for a failed link restoration.
        // Only remove the restored link when the directory positively reports that its user is gone.
        var previousUser = new User { Id = oldLink.UserId, TenantId = oldLink.TenantId };
        if (!await _userProvisioningService.ExistsAsync(previousUser, false, cancellationToken))
        {
            try
            {
                await using var cleanupContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
                await cleanupContext.ExternalIdentityLinks.Where(x => x.Id == oldLink.Id).ExecuteDeleteAsync(cancellationToken);
            }
            catch (Exception cleanupException)
            {
                await RemoveReplacementLinksOrThrowAsync(oldLink.Id, replacementLink.Id, cleanupException, cancellationToken);
                throw new InvalidOperationException(
                    "The restored previous link was removed after its user was deleted, but its first cleanup attempt failed.",
                    cleanupException);
            }
        }
    }

    private async ValueTask<bool> LinkExistsAsync(string linkId, CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
        return await dbContext.ExternalIdentityLinks.AsNoTracking().AnyAsync(x => x.Id == linkId, cancellationToken);
    }

    private async ValueTask RemoveReplacementLinksOrThrowAsync(
        string oldLinkId,
        string replacementLinkId,
        Exception operationException,
        CancellationToken cancellationToken)
    {

View on GitHub (pinned to fe9217bdfa)