fullstackhero/dotnet-starter-kit · error · InvalidOperationException
Direct messages have fixed membership.
Error message
Direct messages have fixed membership.
What it means
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.
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.
Example fix
// before
if (channel.Type == ChannelType.DirectMessage)
throw new ConflictException("Direct messages have fixed membership.");
channel.AddMember(request.UserId, currentUserId);
// after
if (channel.Type == ChannelType.DirectMessage)
{
var group = ChatChannel.CreateGroup($"dm-{channel.Id}-group", currentUserId);
group.AddMember(request.UserId, currentUserId);
return group;
}
channel.AddMember(request.UserId, currentUserId); Defensive patterns
Strategy: validation
Validate before calling
public static bool CanAddMembers(ChatChannel c) => c.Type != ChannelType.DirectMessage;
Type guard
if (channel.Type == ChannelType.DirectMessage) throw new ConflictException("Direct messages have fixed membership."); Try / catch
try { channel.AddMember(userId, addedBy); } catch (InvalidOperationException ex) when (ex.Message.Contains("fixed membership")) { throw new ConflictException(ex.Message); } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Only named Channels can change privacy.
- User is already a member.
- User is not a member.
- Cannot start a DM with yourself.
- Group DM requires at least 3 distinct members.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/6e51568287a7c876.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs:176
UpdatedAtUtc = DateTime.UtcNow;
}
public void SetPrivate(bool isPrivate)
{
if (Type != ChannelType.Channel)
{
throw new InvalidOperationException("Only named Channels can change privacy.");
}
IsPrivate = isPrivate;
UpdatedAtUtc = DateTime.UtcNow;
}
public ChannelMember AddMember(string userId, string addedByUserId, ChannelMemberRole role = ChannelMemberRole.Member)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
if (Type == ChannelType.DirectMessage)
{
throw new InvalidOperationException("Direct messages have fixed membership.");
}
if (_members.Any(m => string.Equals(m.UserId, userId, StringComparison.Ordinal)))
{
throw new InvalidOperationException($"User {userId} is already a member.");
}
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)View on GitHub (pinned to 3f2959e683)