{"record":{"id":"b73a9569856bc452","repo":"elsa-workflows/elsa-core","slug":"a-role-with-id-roleid-already-exists","errorCode":null,"errorMessage":"A role with ID '{roleId}' already exists.","messagePattern":"A role with ID '(.+?)' already exists\\.","errorType":"validation","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Identity/Services/RoleManager.cs","lineNumber":24,"sourceCode":"\nnamespace Elsa.Identity.Services;\n\n/// <summary>\n/// Default implementation of <see cref=\"IRoleManager\"/>.\n/// </summary>\npublic class RoleManager(IRoleStore roleStore, IRoleProvider roleProvider, ITenantAccessor tenantAccessor) : IRoleManager\n{\n    /// <inheritdoc />\n    public async Task<CreateRoleResult> CreateRoleAsync(\n        string name,\n        ICollection<string>? permissions = null,\n        string? id = null,\n        CancellationToken cancellationToken = default)\n    {\n        var roleId = id ?? name.Kebaberize();\n\n        if (await RoleExistsAsync(roleId, cancellationToken))\n            throw new InvalidOperationException($\"A role with ID '{roleId}' already exists.\");\n\n        var role = new Role\n        {\n            Id = roleId,\n            Name = name,\n            // The in-memory path does not run EF's ApplyTenantId saving handler.\n            TenantId = tenantAccessor.TenantId,\n            Permissions = permissions ?? new List<string>()\n        };\n\n        await roleStore.SaveAsync(role, cancellationToken);\n\n        return new CreateRoleResult(role);\n    }\n\n    private async Task<bool> RoleExistsAsync(string roleId, CancellationToken cancellationToken)\n    {\n        var storedRole = await roleStore.FindAsync(new() { Id = roleId }, cancellationToken);","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Identity/Services/RoleManager.cs#L6-L42","documentation":"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.","triggerScenarios":"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').","commonSituations":"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.","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."],"exampleFix":"// before\nawait roleManager.CreateRoleAsync(name: \"admin\", id: \"admin\"); // throws if run twice\n// after\nif (!await roleManager.RoleExistsAsync(\"admin\"))\n    await roleManager.CreateRoleAsync(name: \"admin\", id: \"admin\");","handlingStrategy":"try-catch","validationCode":"if (await roleManager.RoleExistsAsync(roleId)) return; // skip creation","typeGuard":null,"tryCatchPattern":"try { await roleManager.CreateRoleAsync(name: name, id: roleId); }\ncatch (InvalidOperationException ex) when (ex.Message.StartsWith(\"A role with ID\"))\n{\n    logger.LogDebug(\"Role {RoleId} already exists; skipping creation\", roleId);\n}","preventionTips":["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."],"tags":["identity","roles","duplicate","seed-data"],"backgroundTag":"file-already-exists","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}