{"record":{"id":"6e51568287a7c876","repo":"fullstackhero/dotnet-starter-kit","slug":"direct-messages-have-fixed-membership","errorCode":null,"errorMessage":"Direct messages have fixed membership.","messagePattern":"Direct messages have fixed membership\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs","lineNumber":176,"sourceCode":"        UpdatedAtUtc = DateTime.UtcNow;\n    }\n\n    public void SetPrivate(bool isPrivate)\n    {\n        if (Type != ChannelType.Channel)\n        {\n            throw new InvalidOperationException(\"Only named Channels can change privacy.\");\n        }\n        IsPrivate = isPrivate;\n        UpdatedAtUtc = DateTime.UtcNow;\n    }\n\n    public ChannelMember AddMember(string userId, string addedByUserId, ChannelMemberRole role = ChannelMemberRole.Member)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(userId);\n        if (Type == ChannelType.DirectMessage)\n        {\n            throw new InvalidOperationException(\"Direct messages have fixed membership.\");\n        }\n        if (_members.Any(m => string.Equals(m.UserId, userId, StringComparison.Ordinal)))\n        {\n            throw new InvalidOperationException($\"User {userId} is already a member.\");\n        }\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)","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs#L158-L194","documentation":"ChatChannel.AddMember blocks membership changes for DirectMessage channels: DM participants are fixed at creation and no one can be added afterwards. The aggregate throws InvalidOperationException when Type == ChannelType.DirectMessage. This keeps DM membership invariants intact.","triggerScenarios":"Calling chatChannel.AddMember(userId, addedByUserId, role) on a channel whose Type is ChannelType.DirectMessage. Reached e.g. via an 'invite to channel' command that doesn't distinguish DMs from group channels.","commonSituations":"Generic invite/add-member endpoint used for both group channels and DMs; UI offering 'add member' on a DM; flows attempting to 'upgrade' a 1:1 DM into a group by adding users instead of creating a new group channel.","solutions":["Check channel.Type != ChannelType.DirectMessage before calling AddMember and reject/skip the operation for DMs.","Create a new group Channel and re-add the intended members rather than mutating the DM.","In the API layer, return 400/422 ('cannot add members to a DM') from the invite command handler before touching the aggregate.","Hide or disable member-invitation UI when the channel is a DM."],"exampleFix":"// before\nif (channel.Type == ChannelType.DirectMessage)\n    throw new ConflictException(\"Direct messages have fixed membership.\");\nchannel.AddMember(request.UserId, currentUserId);\n// after\nif (channel.Type == ChannelType.DirectMessage)\n{\n    var group = ChatChannel.CreateGroup($\"dm-{channel.Id}-group\", currentUserId);\n    group.AddMember(request.UserId, currentUserId);\n    return group;\n}\nchannel.AddMember(request.UserId, currentUserId);","handlingStrategy":"validation","validationCode":"public static bool CanAddMembers(ChatChannel c) => c.Type != ChannelType.DirectMessage;","typeGuard":"if (channel.Type == ChannelType.DirectMessage) throw new ConflictException(\"Direct messages have fixed membership.\");","tryCatchPattern":"try { channel.AddMember(userId, addedBy); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"fixed membership\")) { throw new ConflictException(ex.Message); }","preventionTips":["Hide 'add member' affordances for DM conversations","Route DM 'add user' intents to create-a-new-group flow","Validate channel type in the invite command handler before loading the aggregate","Document DM membership invariants in the API contract"],"tags":["domain","chat","membership","invalid-operation"],"backgroundTag":"invalid-state-transition","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"}