{"record":{"id":"ed22b153710bc553","repo":"fullstackhero/dotnet-starter-kit","slug":"user-command-userid-is-not-a-member-of-group-command-groupid","errorCode":null,"errorMessage":"User '{command.UserId}' is not a member of group '{command.GroupId}'.","messagePattern":"User '(.+?)' is not a member of group '(.+?)'\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Groups/RemoveUserFromGroup/RemoveUserFromGroupCommandHandler.cs","lineNumber":31,"sourceCode":"    private readonly IUserPermissionService _userPermissionService;\n\n    public RemoveUserFromGroupCommandHandler(IdentityDbContext dbContext, IUserPermissionService userPermissionService)\n    {\n        _dbContext = dbContext;\n        _userPermissionService = userPermissionService;\n    }\n\n    public async ValueTask<Unit> Handle(RemoveUserFromGroupCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var membership = await _dbContext.UserGroups\n            .Include(ug => ug.Group)\n            .FirstOrDefaultAsync(ug => ug.GroupId == command.GroupId && ug.UserId == command.UserId, cancellationToken);\n\n        if (membership is null)\n        {\n            throw new NotFoundException($\"User '{command.UserId}' is not a member of group '{command.GroupId}'.\");\n        }\n\n        // Default groups (e.g. seeded \"All Users\") require every tenant user to be a member, so\n        // removing one breaks that invariant and leaves later registrants in a half-populated group.\n        if (membership.Group is not null && membership.Group.IsDefault)\n        {\n            throw new ForbiddenException(\"Users cannot be removed from a default group.\");\n        }\n\n        _dbContext.UserGroups.Remove(membership);\n        await _dbContext.SaveChangesAsync(cancellationToken);\n\n        // Leaving a group may revoke roles the user only held through this group —\n        // invalidate so the cached permission set is rebuilt on next request.\n        await _userPermissionService.InvalidatePermissionCacheAsync(command.UserId, cancellationToken).ConfigureAwait(false);\n\n        return Unit.Value;\n    }","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Groups/RemoveUserFromGroup/RemoveUserFromGroupCommandHandler.cs#L13-L49","documentation":"RemoveUserFromGroup looks up the UserGroups membership row for the (GroupId, UserId) pair and throws NotFoundException if none exists — the 404 message names both ids. It distinguishes 'not a member' from 'group missing' by including both ids in the text.","triggerScenarios":"DELETE /groups/{groupId}/users/{userId} where the user was never added, already removed (double delete), or either id is cross-tenant/invalid.","commonSituations":"Idempotent retry of a remove that already succeeded; removing a user whose membership row was cascaded away when the group or user was deleted; swapped groupId/userId in the URL.","solutions":["Treat 404 as success in idempotent removal flows (desired end state already reached)","Check membership via GET /groups/{groupId}/members before removing","Verify you didn't swap the groupId and userId path segments"],"exampleFix":"// before\nawait api.RemoveUserFromGroup(groupId, userId); // throws on retry\n// after\ntry { await api.RemoveUserFromGroup(groupId, userId); }\ncatch (ApiException e) when (e.Status == 404) { /* already removed */ }","handlingStrategy":"try-catch","validationCode":"const members = await api.GetGroupMembers(groupId);\nif (!members.some(m => m.userId === userId)) return; // nothing to remove","typeGuard":null,"tryCatchPattern":"try { await api.RemoveUserFromGroup(groupId, userId); }\ncatch (ApiException e) when (e.Status == 404) { /* idempotent: not a member */ }","preventionTips":["Design remove-membership as idempotent (404 = already done)","Verify URL parameter order (groupId then userId)","Check membership before remove in UI flows"],"tags":["http-404","identity","groups","membership"],"backgroundTag":"entity-not-found","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"}