elsa-workflows/elsa-core · error · InvalidOperationException

A unique Elsa user name could not be reserved for the…

Error message

A unique Elsa user name could not be reserved for the external identity.

What it means

After MaximumUserNameAttempts (10) generated candidate names, ResolveAsync gives up and throws. A name is only usable when the optional tryReserveUserName callback accepts it AND no user with that name already exists in the user provider; on a failed save, a name collision with the same name also aborts the loop. This means the service could not generate a unique, reservable user name for the new external identity.

Solutions

  1. Inspect the tryReserveUserName callback: it must return true for available names; fix any backing reservation table/lock that is rejecting all candidates.
  2. Retry the login/provisioning flow — collisions are expected to be transient given random generated IDs.
  3. Check the user store for a large number of pre-existing names matching the configured prefix and change the UserNamePrefix in the proposal.
  4. Investigate persistence errors during SaveAsync (the real failure may be masked when a same-named user appears in the recovery check).

Example fix

// before: callback that always fails under contention
bool TryReserve(string name) => linkTable.TryAdd(name, externalId); // table locked/full
// after: surface the underlying cause and allow retry
bool TryReserve(string name)
{
    try { return linkTable.TryAdd(name, externalId); }
    catch (Exception ex) { logger.LogError(ex, "Name reservation failed"); throw; }
}
Defensive patterns

Strategy: retry

Validate before calling

bool TryReserve(string name) => reservationTable.TryReserve(name, externalId); // must be a healthy, non-always-false reservation check

Try / catch

try
{
    var (user, created) = await provisioning.ResolveAsync(request, ct: ct);
}
catch (InvalidOperationException ex) when (ex.Message == "A unique Elsa user name could not be reserved for the external identity.")
{
    logger.LogError(ex, "Could not reserve a unique user name after 10 attempts");
    throw; // investigate reservation callback / persistence health before retrying
}

Prevention

When it happens

Trigger: Calling ResolveAsync without ExistingUserId when every generated candidate name (prefix-<id>) is either rejected by tryReserveUserName or already exists in the user store — e.g. a reservation callback that always returns false, a legacy user store full of colliding names, or repeated saves failing while a same-named user gets persisted by a concurrent process.

Common situations: Custom tryReserveUserName implementations (e.g. external identity-link tables) that permanently reject names due to a full/corrupt link table; extremely high name collisions after data imports; concurrent provisioning storms creating the same prefix names; database failures during SaveAsync followed by a name-only match that hides the real error.

Related errors


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

Appendix: source

Thrown at src/modules/Elsa.ExternalAuthentication/Services/ExternalIdentityUserProvisioningService.cs:88

                    { Id = user.Id }, CancellationToken.None);
                if (persistedUser is not null)
                    await userStore.DeleteAsync(new()
                        { Id = user.Id }, CancellationToken.None);
                throw;
            }
            catch
            {
                var persistedUser = await userProvider.FindAsync(new()
                    { Id = user.Id }, cancellationToken);
                if (persistedUser is not null)
                    return (persistedUser, true);
                if (await userProvider.FindAsync(new()
                        { Name = name }, cancellationToken) is null)
                    throw;
            }
        }

        throw new InvalidOperationException("A unique Elsa user name could not be reserved for the external identity.");
    }

    /// <summary>
    /// Removes a user created by an operation that could not publish its external identity link.
    /// </summary>
    public Task RemoveAsync(User user, CancellationToken cancellationToken = default) =>
        userStore.DeleteAsync(new()
            { Id = user.Id }, cancellationToken);

    /// <summary>
    /// Checks that the resolved user still exists in the source that supplied it.
    /// </summary>
    public async ValueTask<bool> ExistsAsync(User user, bool wasCreated, CancellationToken cancellationToken = default) =>
        wasCreated
            ? await userStore.FindAsync(new()
                { Id = user.Id }, cancellationToken) is not null
            : await userProvider.FindAsync(new()
                { Id = user.Id }, cancellationToken) is not null;

View on GitHub (pinned to fe9217bdfa)