fullstackhero/dotnet-starter-kit · error · InvalidOperationException
User is not a member.
Error message
User {userId} is not a member. What it means
ChatChannel.RemoveMember requires the target user to actually be a member; when no ChannelMember matches the given UserId (ordinal comparison), the aggregate throws InvalidOperationException naming the user. The guard uses the ?? throw pattern after FirstOrDefault over _members.
Solutions
- Check membership before removing (Members.Any(m => m.UserId == userId)) and treat 'not a member' as a no-op for idempotent removal.
- Catch the InvalidOperationException in the handler and return 404/409 with a clear message instead of a 500.
- Verify the channel id and user id in the request (ordinal-exact match); normalize user ids to a canonical case at creation.
- Load the aggregate fresh in the same unit of work so the membership check sees committed data.
Example fix
// before
channel.RemoveMember(request.UserId, currentUserId);
// after
if (channel.Members.All(m => m.UserId != request.UserId))
{
return Result.Success(); // idempotent: nothing to remove
}
channel.RemoveMember(request.UserId, currentUserId); Defensive patterns
Strategy: validation
Validate before calling
public static bool IsMember(ChatChannel c, string userId) => c.Members.Any(m => string.Equals(m.UserId, userId, StringComparison.Ordinal));
Type guard
if (!channel.Members.Any(m => m.UserId == userId)) return Result.Success();
Try / catch
try { channel.RemoveMember(userId, removedBy); } catch (InvalidOperationException ex) when (ex.Message.Contains("is not a member")) { throw new NotFoundException(nameof(ChannelMember), userId); } Prevention
- Treat repeated removes as idempotent no-ops
- Confirm channel/user ids in requests (watch case and format)
- Refresh client member lists on ChannelMemberRemoved events
- Validate the user is a member at the handler layer before removal
When it happens
Trigger: Calling chatChannel.RemoveMember(userId, removedByUserId) where userId has no membership in the channel — wrong channel id in the request, the user already left/was removed (retried command), or case-mismatched user identifiers.
Common situations: Duplicate 'remove member' requests (first succeeded, second throws); client removing a user from the wrong channel; legacy users whose ids changed casing; moderation scripts iterating stale member lists.
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
- User is already a member.
- Direct messages have fixed membership.
- Channel not found.
- Image not found on product .
- Cannot start a DM with yourself.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/c27a97f23e4064dd.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs:199
}
var member = ChannelMember.Create(Id, userId, role);
_members.Add(member);
UpdatedAtUtc = DateTime.UtcNow;
AddDomainEvent(DomainEvent.Create((id, ts) =>
new ChannelMemberAddedDomainEvent(Id, userId, addedByUserId, id, ts)));
return member;
}
public void RemoveMember(string userId, string removedByUserId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
if (Type == ChannelType.DirectMessage)
{
throw new InvalidOperationException("Direct messages have fixed membership.");
}
var member = _members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal))
?? throw new InvalidOperationException($"User {userId} is not a member.");
_members.Remove(member);
UpdatedAtUtc = DateTime.UtcNow;
AddDomainEvent(DomainEvent.Create((id, ts) =>
new ChannelMemberRemovedDomainEvent(Id, userId, removedByUserId, id, ts)));
}
public void MarkRead(string userId, Guid messageId)
{
var member = _members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal))
?? throw new InvalidOperationException($"User {userId} is not a member.");
member.MarkRead(messageId);
}
public void TouchLastMessage(DateTime utcNow)
{
LastMessageAtUtc = utcNow;
UpdatedAtUtc = utcNow;
}View on GitHub (pinned to 3f2959e683)