fullstackhero/dotnet-starter-kit · error · CustomException

operation failed

Error message

operation failed

What it means

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).

Solutions

  1. Retry the whole UpdatePermissionsAsync call after re-reading the role
  2. Serialize permission updates per role (optimistic concurrency or lock)
  3. Inspect result.Errors in the CustomException data for the underlying Identity reason

Example fix

// before
await roleService.UpdatePermissionsAsync(roleId, perms, ct);
// after
try { await roleService.UpdatePermissionsAsync(roleId, perms, ct); }
catch (CustomException) { var fresh = await GetFreshPermsAsync(roleId, ct); await roleService.UpdatePermissionsAsync(roleId, Merge(fresh, perms), ct); }
Defensive patterns

Strategy: retry

Validate before calling

var current = (await roleService.GetWithPermissionsAsync(roleId, ct)).Permissions;
var toRemove = current.Except(desired).ToList(); // log this; empty list => no RemoveClaimAsync path

Try / catch

try { await roleService.UpdatePermissionsAsync(roleId, desired, ct); }
catch (CustomException ex) {
  logger.LogWarning(ex, "Permission update failed for {RoleId}: {@Errors}", roleId, ex.Data["errors"]);
  await Task.Delay(200, ct); // then retry once with fresh state
}

Prevention

When it happens

Trigger: 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.

Common situations: Two admins saving the same role's permissions simultaneously (concurrency stamp mismatch); DB connection drop mid-update; claim row already removed by another request.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/2b25d56e5620c773. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs:217

        // Strip every permission flagged IsRoot in the registry. (A prior prefix check on "Permissions.Root."
        // was a no-op — no root perm uses that prefix — letting a tenant admin grant themselves root perms.)
        var rootOnly = PermissionConstants.Root.Select(p => p.Name).ToHashSet(StringComparer.Ordinal);
        permissions.RemoveAll(rootOnly.Contains);
    }

    private async Task RemoveRevokedPermissionsAsync(FshRole role, IList<System.Security.Claims.Claim> currentClaims, List<string> permissions, CancellationToken cancellationToken = default)
    {
        var claimsToRemove = currentClaims.Where(c => !permissions.Exists(p => p == c.Value));

        foreach (var claim in claimsToRemove)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var result = await roleManager.RemoveClaimAsync(role, claim);
            if (!result.Succeeded)
            {
                var errors = result.Errors.Select(error => error.Description).ToList();
                throw new CustomException("operation failed", errors);
            }
        }
    }

    private async Task AddNewPermissionsAsync(FshRole role, IList<System.Security.Claims.Claim> currentClaims, List<string> permissions, CancellationToken cancellationToken = default)
    {
        var newPermissions = permissions
            .Where(p => !string.IsNullOrEmpty(p) && !currentClaims.Any(c => c.Value == p))
            .ToList();

        foreach (string permission in newPermissions)
        {
            context.RoleClaims.Add(new FshRoleClaim
            {
                RoleId = role.Id,
                ClaimType = ClaimConstants.Permission,
                ClaimValue = permission,
                CreatedBy = currentUser.GetUserId().ToString()

View on GitHub (pinned to 3f2959e683)