fullstackhero/dotnet-starter-kit · error · NotFoundException
User with ID ' ' not found.
Error message
User with ID '{query.UserId}' not found. What it means
GetUserGroupsQueryHandler first checks that the requested user exists in the Users table; if no row matches query.UserId it throws NotFoundException with a message naming the ID. This distinguishes a bad user ID from a user that simply has no groups.
Solutions
- Verify the userId exists: query Users by Id (with the correct tenant context) before calling the endpoint.
- Check the X-Tenant-Id / tenant resolution — the user may exist in a different tenant than the one the request resolves to.
- Ensure the client stores and sends the correct, current user id; refresh cached user lists after deletions.
Example fix
// before
const res = await apiFetch(`/api/v1/users/${staleId}/user-groups`);
// after
const users = await searchUsers(email);
if (!users.length) throw new Error(`User ${email} not found`);
const res = await apiFetch(`/api/v1/users/${users[0].id}/user-groups`); Defensive patterns
Strategy: validation
Validate before calling
const userExists = await checkUserExists(userId, tenantId);
if (!userExists) throw new ApiError(404, `User ${userId} not found`);
// then call GET /users/{userId}/user-groups Type guard
function hasValidUserId(id) {
return typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
} Try / catch
try { return await apiFetch(`/users/${id}/user-groups`); }
catch (e) {
if (e.status === 404) { refreshUserCache(); return []; }
throw e;
} Prevention
- Refresh cached user lists after delete operations.
- Always send the tenant header matching the user's tenant.
- Validate Guid format client-side before calling.
- Treat 404 on user-scoped endpoints as possible cross-tenant isolation, not just missing data.
When it happens
Trigger: GET user-groups with a userId that was deleted, belongs to another tenant (global query filters exclude it), is a malformed/never-existing Guid, or referencing a soft-deleted user.
Common situations: Stale links/bookmarks after user removal; cross-tenant data access where Finbuckle's tenant filter hides the row; frontend passing the admin's id instead of the target user's; importing IDs from a different database.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/f3941c9e2cc0e6a6.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserGroups/GetUserGroupsQueryHandler.cs:28
public sealed class GetUserGroupsQueryHandler : IQueryHandler<GetUserGroupsQuery, IEnumerable<GroupDto>>
{
private readonly IdentityDbContext _dbContext;
public GetUserGroupsQueryHandler(IdentityDbContext dbContext)
{
_dbContext = dbContext;
}
public async ValueTask<IEnumerable<GroupDto>> Handle(GetUserGroupsQuery query, CancellationToken cancellationToken)
{
// Validate user exists
var userExists = await _dbContext.Users
.AsNoTracking()
.AnyAsync(u => u.Id == query.UserId, cancellationToken);
if (!userExists)
{
throw new NotFoundException($"User with ID '{query.UserId}' not found.");
}
// Get user's groups
var groupIds = await _dbContext.UserGroups
.AsNoTracking()
.Where(ug => ug.UserId == query.UserId)
.Select(ug => ug.GroupId)
.ToListAsync(cancellationToken);
if (groupIds.Count == 0)
{
return [];
}
var groups = await _dbContext.Groups
.AsNoTracking()
.Include(g => g.GroupRoles)
.Where(g => groupIds.Contains(g.Id))View on GitHub (pinned to 3f2959e683)