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

  1. Check channel.Type != ChannelType.DirectMessage before calling AddMember and reject/skip the operation for DMs.
  2. Create a new group Channel and re-add the intended members rather than mutating the DM.
  3. In the API layer, return 400/422 ('cannot add members to a DM') from the invite command handler before touching the aggregate.
  4. 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

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


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)