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
- Retry the sign-in/replace operation once the database is healthy; compensation will re-run from the current link state.
- Check the inner cleanupException for transient errors and configure the DbContext/retry strategy (EnableRetryOnFailure) for transient faults.
- Inspect ExternalIdentityLinks for leftover old/replacement link rows and delete any pointing to deleted users.
- 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
- Enable transient-fault retry (EnableRetryOnFailure) on the DbContext
- Schedule cleanup jobs to avoid deadlocking with sign-in provisioning
- Watch for cancellation-prone long compensation paths
- Audit link rows after DB incidents for orphans pointing to deleted users
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
- The replacement link was removed after its target user was…
- The Elsa user was deleted while its external identity link…
- A replacement-compensation link refers to a deleted user…
- Register with configured before calling , or call with a…
- AI conversation entity metadata was not found.
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)