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

  1. Check role existence before creating: only call CreateRoleAsync when FindByIdAsync/RoleExistsAsync returns no match.
  2. Make role seeding idempotent by upserting (update name/permissions if the role exists, create otherwise).
  3. 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.
  4. 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

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


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)