elsa-workflows/elsa-core · error · InvalidOperationException
A role with ID ' ' already exists.
Error message
A role with ID '{roleId}' already exists. What it means
RoleManager.CreateRoleAsync derives the role ID from the provided id or by kebab-casing the name, then checks RoleExistsAsync. If a role with that ID is already present, it throws InvalidOperationException instead of silently overwriting. This guards the identity role registry against duplicate role IDs.
Solutions
- Check role existence before creating: only call CreateRoleAsync when FindByIdAsync/RoleExistsAsync returns no match.
- Make role seeding idempotent by upserting (update name/permissions if the role exists, create otherwise).
- Pass a unique id or unique name for each new role; remember the name is kebab-cased, so distinct names must kebab-case to distinct IDs.
- Wrap creation in try/catch for InvalidOperationException if duplicate creation is expected to be benign and should be ignored.
Example fix
// before
await roleManager.CreateRoleAsync(name: "admin", id: "admin"); // throws if run twice
// after
if (!await roleManager.RoleExistsAsync("admin"))
await roleManager.CreateRoleAsync(name: "admin", id: "admin"); Defensive patterns
Strategy: try-catch
Validate before calling
if (await roleManager.RoleExistsAsync(roleId)) return; // skip creation
Try / catch
try { await roleManager.CreateRoleAsync(name: name, id: roleId); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("A role with ID"))
{
logger.LogDebug("Role {RoleId} already exists; skipping creation", roleId);
} Prevention
- Guard all role seeding with an existence check.
- Remember role names are kebab-cased into IDs; avoid names that collapse to the same ID.
- Run seed scripts once or make them safe to re-run.
When it happens
Trigger: Calling CreateRoleAsync with an explicit id (e.g. an admin role ID) that already exists, or calling it twice with the same name since the name is kebab-cased into the same role ID (e.g. name 'Admin' and 'admin' both produce 'admin').
Common situations: Seeding default roles on every startup without checking existence first, test fixtures (like CreateRoleRejectsProvidedAdminRoleIdCollision) passing a fixed role ID across multiple creations, or re-running migrations/seed scripts against a persistent role store.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- A configured default role no longer exists.
- A already exists with ' ' in tenant ' '.
- This external identity provisioner does not support atomic…
- Adapter ' ' registers more than one migration from version .
- The requested Elsa user does not exist.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/b73a9569856bc452.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Identity/Services/RoleManager.cs:24
namespace Elsa.Identity.Services;
/// <summary>
/// Default implementation of <see cref="IRoleManager"/>.
/// </summary>
public class RoleManager(IRoleStore roleStore, IRoleProvider roleProvider, ITenantAccessor tenantAccessor) : IRoleManager
{
/// <inheritdoc />
public async Task<CreateRoleResult> CreateRoleAsync(
string name,
ICollection<string>? permissions = null,
string? id = null,
CancellationToken cancellationToken = default)
{
var roleId = id ?? name.Kebaberize();
if (await RoleExistsAsync(roleId, cancellationToken))
throw new InvalidOperationException($"A role with ID '{roleId}' already exists.");
var role = new Role
{
Id = roleId,
Name = name,
// The in-memory path does not run EF's ApplyTenantId saving handler.
TenantId = tenantAccessor.TenantId,
Permissions = permissions ?? new List<string>()
};
await roleStore.SaveAsync(role, cancellationToken);
return new CreateRoleResult(role);
}
private async Task<bool> RoleExistsAsync(string roleId, CancellationToken cancellationToken)
{
var storedRole = await roleStore.FindAsync(new() { Id = roleId }, cancellationToken);View on GitHub (pinned to fe9217bdfa)