fullstackhero/dotnet-starter-kit · error · NotFoundException

Roles not found

Error message

Roles not found: {string.Join(", ", invalidRoleIds)}

What it means

ValidateRoleIdsAsync loads the IDs of roles matching the requested roleIds and throws NotFoundException listing every requested ID that does not exist. UpdateGroup therefore refuses partial role updates: all supplied RoleIds must resolve to real roles.

Solutions

  1. Remove or correct the invalid role IDs reported in the message before resending the command.
  2. Refresh the role list from the API and rebuild the RoleIds selection from current data.
  3. Create the missing roles first if they are genuinely required, then retry the group update.

Example fix

// before
var cmd = new UpdateGroupCommand { Id = groupId, RoleIds = ["role-1", "role-deleted"] };

// after
var roles = await roleApi.ListAsync();
var validIds = cmd.RoleIds.Where(id => roles.Any(r => r.Id == id)).ToList();
if (validIds.Count != cmd.RoleIds.Count)
    showWarning("Some roles no longer exist and were removed.");
await mediator.Send(new UpdateGroupCommand { Id = groupId, RoleIds = validIds });
Defensive patterns

Strategy: validation

Validate before calling

var roles = await roleApi.ListAsync();
var roleIds = new HashSet<string>(selectedRoleIds);
if (!roleIds.IsSubsetOf(roles.Select(r => r.Id)))
{
    showValidationError("Some selected roles no longer exist. Refresh and reselect.");
}

Type guard

string[] validRoleIds(IEnumerable<string> requested, IEnumerable<Role> existing) =>
    requested.Where(id => existing.Any(r => r.Id == id)).ToArray();

Try / catch

try
{
    await mediator.Send(new UpdateGroupCommand { Id = id, RoleIds = roleIds });
}
catch (NotFoundException ex)
{
    // message lists the missing role IDs
    await refreshRoleList();
    notify($"Roles not found — reselect: {ex.Message}");
}

Prevention

When it happens

Trigger: Sending UpdateGroupCommand with RoleIds containing one or more IDs that do not exist in the Roles table (deleted roles, IDs from another tenant/environment, malformed GUID strings that parse but match nothing).

Common situations: Frontend caches role options and a role was deleted meanwhile; copying role assignments from a template created in another tenant; importing group configuration JSON from a different environment.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Groups/UpdateGroup/UpdateGroupCommandHandler.cs:101

        }
    }

    private async Task ValidateRoleIdsAsync(IReadOnlyList<string>? roleIds, CancellationToken cancellationToken)
    {
        if (roleIds is not { Count: > 0 })
        {
            return;
        }

        var existingRoleIds = await _dbContext.Roles
            .Where(r => roleIds.Contains(r.Id))
            .Select(r => r.Id)
            .ToListAsync(cancellationToken);

        var invalidRoleIds = roleIds.Except(existingRoleIds).ToList();
        if (invalidRoleIds.Count > 0)
        {
            throw new NotFoundException($"Roles not found: {string.Join(", ", invalidRoleIds)}");
        }
    }

    private static HashSet<string> UpdateRoleAssignments(Group group, IReadOnlyList<string>? roleIds)
    {
        var currentRoleIds = group.GroupRoles.Select(gr => gr.RoleId).ToHashSet();
        var newRoleIds = roleIds?.ToHashSet() ?? [];

        var rolesToRemove = group.GroupRoles.Where(gr => !newRoleIds.Contains(gr.RoleId)).ToList();
        foreach (var role in rolesToRemove)
        {
            group.GroupRoles.Remove(role);
        }

        foreach (var roleId in newRoleIds.Where(id => !currentRoleIds.Contains(id)))
        {
            group.GroupRoles.Add(GroupRole.Create(group.Id, roleId));
        }

View on GitHub (pinned to 3f2959e683)