{"record":{"id":"2b25d56e5620c773","repo":"fullstackhero/dotnet-starter-kit","slug":"operation-failed","errorCode":null,"errorMessage":"operation failed","messagePattern":"operation failed","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs","lineNumber":217,"sourceCode":"\n        // Strip every permission flagged IsRoot in the registry. (A prior prefix check on \"Permissions.Root.\"\n        // was a no-op — no root perm uses that prefix — letting a tenant admin grant themselves root perms.)\n        var rootOnly = PermissionConstants.Root.Select(p => p.Name).ToHashSet(StringComparer.Ordinal);\n        permissions.RemoveAll(rootOnly.Contains);\n    }\n\n    private async Task RemoveRevokedPermissionsAsync(FshRole role, IList<System.Security.Claims.Claim> currentClaims, List<string> permissions, CancellationToken cancellationToken = default)\n    {\n        var claimsToRemove = currentClaims.Where(c => !permissions.Exists(p => p == c.Value));\n\n        foreach (var claim in claimsToRemove)\n        {\n            cancellationToken.ThrowIfCancellationRequested();\n            var result = await roleManager.RemoveClaimAsync(role, claim);\n            if (!result.Succeeded)\n            {\n                var errors = result.Errors.Select(error => error.Description).ToList();\n                throw new CustomException(\"operation failed\", errors);\n            }\n        }\n    }\n\n    private async Task AddNewPermissionsAsync(FshRole role, IList<System.Security.Claims.Claim> currentClaims, List<string> permissions, CancellationToken cancellationToken = default)\n    {\n        var newPermissions = permissions\n            .Where(p => !string.IsNullOrEmpty(p) && !currentClaims.Any(c => c.Value == p))\n            .ToList();\n\n        foreach (string permission in newPermissions)\n        {\n            context.RoleClaims.Add(new FshRoleClaim\n            {\n                RoleId = role.Id,\n                ClaimType = ClaimConstants.Permission,\n                ClaimValue = permission,\n                CreatedBy = currentUser.GetUserId().ToString()","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs#L199-L235","documentation":"RemoveRevokedPermissionsAsync calls roleManager.RemoveClaimAsync per revoked claim and throws CustomException(\"operation failed\") with the Identity error descriptions when the removal does not succeed (result.Succeeded == false).","triggerScenarios":"The role store rejects the claim removal — e.g. the role was deleted concurrently, concurrency stamp changed (row updated elsewhere), or the database is unavailable.","commonSituations":"Two admins saving the same role's permissions simultaneously (concurrency stamp mismatch); DB connection drop mid-update; claim row already removed by another request.","solutions":["Retry the whole UpdatePermissionsAsync call after re-reading the role","Serialize permission updates per role (optimistic concurrency or lock)","Inspect result.Errors in the CustomException data for the underlying Identity reason"],"exampleFix":"// before\nawait roleService.UpdatePermissionsAsync(roleId, perms, ct);\n// after\ntry { await roleService.UpdatePermissionsAsync(roleId, perms, ct); }\ncatch (CustomException) { var fresh = await GetFreshPermsAsync(roleId, ct); await roleService.UpdatePermissionsAsync(roleId, Merge(fresh, perms), ct); }","handlingStrategy":"retry","validationCode":"var current = (await roleService.GetWithPermissionsAsync(roleId, ct)).Permissions;\nvar toRemove = current.Except(desired).ToList(); // log this; empty list => no RemoveClaimAsync path","typeGuard":null,"tryCatchPattern":"try { await roleService.UpdatePermissionsAsync(roleId, desired, ct); }\ncatch (CustomException ex) {\n  logger.LogWarning(ex, \"Permission update failed for {RoleId}: {@Errors}\", roleId, ex.Data[\"errors\"]);\n  await Task.Delay(200, ct); // then retry once with fresh state\n}","preventionTips":["Avoid two admins editing the same role's permissions concurrently","Reload permissions before each save instead of merging a stale list","Read the Identity error descriptions carried in the exception for root cause"],"tags":["identity","roles","claims","identity-store"],"backgroundTag":"database-write-failed","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}