elsa-workflows/elsa-core · critical · AggregateException

A replacement-compensation link refers to a deleted user…

Error message

A replacement-compensation link refers to a deleted user and could not be removed. No credentials were issued.

What it means

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.

Solutions

  1. Check both inner exceptions to distinguish the original failure from the cleanup failure and fix the DB/connectivity root cause.
  2. Manually delete ExternalIdentityLinks rows with the recorded link ids that reference deleted users.
  3. Enable transient-fault retry (EnableRetryOnFailure) so cleanup deletes can succeed on retry.
  4. Retry external sign-in after the database is reachable; provisioning will recreate consistent state.

Example fix

// diagnosing
try { await provisioner.ReplaceAsync(...); }
catch (AggregateException agg) when (agg.Message.Contains("replacement-compensation link"))
{
    foreach (var inner in agg.InnerExceptions) logger.LogError(inner, "replace/cleanup failure");
}
Defensive patterns

Strategy: try-catch

Validate before calling

bool orphan = !await userProvisioningService.ExistsAsync(user, false, ct)
              && await LinkExistsAsync(linkId, ct); // link exists but user gone: needs cleanup

Try / catch

try { await provisioner.ReplaceAsync(...); }
catch (AggregateException agg) when (agg.Message.Contains("replacement-compensation link"))
{
    foreach (var inner in agg.InnerExceptions) logger.LogError(inner, "Operation + cleanup both failed");
    // DB/link repair required before issuing any credentials
}

Prevention

When it happens

Trigger: 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.

Common situations: Database outage during federated sign-in compensation; lock contention with cleanup jobs; misconfigured connection string causing repeated write failures during the same request.

Related errors


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

Appendix: source

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

        return await dbContext.ExternalIdentityLinks.AsNoTracking().AnyAsync(x => x.Id == linkId, cancellationToken);
    }

    private async ValueTask RemoveReplacementLinksOrThrowAsync(
        string oldLinkId,
        string replacementLinkId,
        Exception operationException,
        CancellationToken cancellationToken)
    {
        try
        {
            await using var cleanupContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
            await cleanupContext.ExternalIdentityLinks
                .Where(x => x.Id == replacementLinkId || x.Id == oldLinkId)
                .ExecuteDeleteAsync(cancellationToken);
        }
        catch (Exception cleanupException)
        {
            throw new AggregateException(
                "A replacement-compensation link refers to a deleted user and could not be removed. No credentials were issued.",
                operationException,
                cleanupException);
        }
    }

    private async ValueTask RemoveStrandedUserAsync(User user, Exception linkException, CancellationToken cancellationToken)
    {
        try
        {
            await _userProvisioningService.RemoveAsync(user, cancellationToken);
        }
        catch (Exception exception)
        {
            logger.LogError(exception, "Could not remove the just-in-time user {UserId} after its external identity link failed", user.Id);
            throw new AggregateException(
                "External identity provisioning failed and its just-in-time user could not be removed. No credentials were issued.",
                linkException,

View on GitHub (pinned to fe9217bdfa)