{"record":{"id":"c27a97f23e4064dd","repo":"fullstackhero/dotnet-starter-kit","slug":"user-userid-is-not-a-member","errorCode":null,"errorMessage":"User {userId} is not a member.","messagePattern":"User (.+?) is not a member\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs","lineNumber":199,"sourceCode":"        }\n\n        var member = ChannelMember.Create(Id, userId, role);\n        _members.Add(member);\n        UpdatedAtUtc = DateTime.UtcNow;\n        AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new ChannelMemberAddedDomainEvent(Id, userId, addedByUserId, id, ts)));\n        return member;\n    }\n\n    public void RemoveMember(string userId, string removedByUserId)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(userId);\n        if (Type == ChannelType.DirectMessage)\n        {\n            throw new InvalidOperationException(\"Direct messages have fixed membership.\");\n        }\n        var member = _members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal))\n            ?? throw new InvalidOperationException($\"User {userId} is not a member.\");\n        _members.Remove(member);\n        UpdatedAtUtc = DateTime.UtcNow;\n        AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new ChannelMemberRemovedDomainEvent(Id, userId, removedByUserId, id, ts)));\n    }\n\n    public void MarkRead(string userId, Guid messageId)\n    {\n        var member = _members.FirstOrDefault(m => string.Equals(m.UserId, userId, StringComparison.Ordinal))\n            ?? throw new InvalidOperationException($\"User {userId} is not a member.\");\n        member.MarkRead(messageId);\n    }\n\n    public void TouchLastMessage(DateTime utcNow)\n    {\n        LastMessageAtUtc = utcNow;\n        UpdatedAtUtc = utcNow;\n    }","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nchannel.RemoveMember(request.UserId, currentUserId);\n// after\nif (channel.Members.All(m => m.UserId != request.UserId))\n{\n    return Result.Success(); // idempotent: nothing to remove\n}\nchannel.RemoveMember(request.UserId, currentUserId);","handlingStrategy":"validation","validationCode":"public static bool IsMember(ChatChannel c, string userId) => c.Members.Any(m => string.Equals(m.UserId, userId, StringComparison.Ordinal));","typeGuard":"if (!channel.Members.Any(m => m.UserId == userId)) return Result.Success();","tryCatchPattern":"try { channel.RemoveMember(userId, removedBy); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"is not a member\")) { throw new NotFoundException(nameof(ChannelMember), userId); }","preventionTips":["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"],"tags":["domain","chat","membership","not-found","idempotency"],"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"}