elsa-workflows/elsa-core · error · InvalidOperationException

Role ' ' resolves to roles across tenant scopes; the…

Error message

Role '{roleId}' resolves to {roles.Length} roles across tenant scopes; the deletion target is ambiguous and its external-authentication dependencies cannot be determined.

What it means

Error "Role '{roleId}' resolves to {roles.Length} roles across tenant scopes; the deletion target is ambiguous and its external-authentication dependencies cannot be determined." thrown in elsa-workflows/elsa-core.

Solutions

  1. Deduplicate or rename the roles so the role ID is unique across tenants in the store.
  2. Delete the intended role through a tenant-scoped API so exactly one role matches the ID.
  3. If using MemoryRoleStore in tests, give each tenant's role a distinct ID, mirroring the EF Core uniqueness constraint.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs:283 when the library encounters an invalid state.

Common situations: See trigger scenarios.

Understand the failure class


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

Appendix: source

Thrown at src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationRoleDeletionDependencyContributor.cs:283

    /// tenant context is every tenant). In EF Core persistence a role ID is unique across all tenants (the
    /// <c>Roles</c> table keys on <c>Id</c> alone), so this lookup resolves to at most one role there. Only
    /// <c>MemoryRoleStore</c> can hold two roles that share an ID because its storage key includes the tenant;
    /// if the ID resolves to more than one role, which tenant's role the coordinator's own delete actually
    /// targets is already ambiguous, and this method cannot make the operation consistent by guessing in either
    /// direction -- widening would expose another tenant's references for what may be a tenant-scoped deletion,
    /// and narrowing would leave an agnostic role's references dangling. It fails closed instead.
    /// A missing store or no matching role falls back to the ambient tenant on <see cref="ITenantAccessor"/>,
    /// which is the only case where the ambient tenant is trusted: the role cannot be resolved at all, so there
    /// is no resolved tenant to prefer over it.
    /// </summary>
    private async ValueTask<string> ResolveRoleTenantIdAsync(string roleId, CancellationToken cancellationToken)
    {
        var roles = await FindRolesByIdAsync(roleId, cancellationToken);
        return roles.Length switch
        {
            0 => tenantAccessor.TenantId.NormalizeTenantId(),
            1 => roles[0].TenantId.NormalizeTenantId(),
            _ => throw new InvalidOperationException(
                $"Role '{roleId}' resolves to {roles.Length} roles across tenant scopes; the deletion target is ambiguous and its external-authentication dependencies cannot be determined.")
        };
    }

    /// <summary>
    /// Resolves whether a candidate role ID (typically a replacement role) is itself tenant-agnostic. Unlike
    /// <see cref="ResolveRoleTenantIdAsync"/>, an ambiguous candidate -- a role ID that resolves to more than one
    /// role, which only <c>MemoryRoleStore</c> can produce -- is not the coordinator's own deletion target, so
    /// there is no operation to fail closed on by throwing; it is instead treated the same as an unresolved
    /// candidate and reported as not agnostic, since there is no single resolved role to trust as safe to write
    /// into every tenant's connections.
    /// </summary>
    private async ValueTask<bool> IsAgnosticRoleAsync(string? roleId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(roleId))
            return false;
        var roles = await FindRolesByIdAsync(roleId, cancellationToken);
        return roles.Length == 1 && string.Equals(roles[0].TenantId.NormalizeTenantId(), Tenant.AgnosticTenantId, StringComparison.Ordinal);

View on GitHub (pinned to fe9217bdfa)