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
- Deduplicate or rename the roles so the role ID is unique across tenants in the store.
- Delete the intended role through a tenant-scoped API so exactly one role matches the ID.
- 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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
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)