fullstackhero/dotnet-starter-kit · error · InvalidOperationException

Only named Channels can change privacy.

Error message

Only named Channels can change privacy.

What it means

ChatChannel.SetPrivate enforces that privacy (IsPrivate) is only mutable on ChannelType.Channel entities. If the aggregate is a DirectMessage (or any other non-Channel type), the domain model refuses the state change with InvalidOperationException, because DM privacy is fixed by design. This is an intentional domain guard, not a bug.

Solutions

  1. Check Channel.Type before calling SetPrivate and skip the call when Type != ChannelType.Channel (DMs have no privacy concept).
  2. Return a domain/validation error (e.g. 'cannot change privacy of a DM') from the command handler instead of letting the domain exception bubble to a 500.
  3. Restrict the API contract so the isPrivate flag is only accepted for Channel-type channels (conditional validation in the FluentValidation validator).
  4. If the channel should really be a named Channel, fix the data/handlers that created it as a DirectMessage.

Example fix

// before
channel.SetPrivate(request.IsPrivate);
// after
if (channel.Type == ChannelType.Channel)
{
    channel.SetPrivate(request.IsPrivate);
}
else if (request.IsPrivate != channel.IsPrivate)
{
    throw new InvalidOperationException("Only named Channels can change privacy.");
}
Defensive patterns

Strategy: validation

Validate before calling

public static bool CanChangePrivacy(ChatChannel c) => c.Type == ChannelType.Channel;

Type guard

if (channel.Type != ChannelType.Channel) throw new ConflictException("Only named Channels can change privacy.");

Try / catch

try { channel.SetPrivate(request.IsPrivate); } catch (InvalidOperationException ex) when (ex.Message.Contains("privacy")) { throw new ConflictException(ex.Message); }

Prevention

When it happens

Trigger: Calling chatChannel.SetPrivate(true|false) on a ChatChannel whose Type is ChannelType.DirectMessage (or any type other than Channel). Typically reached via a 'update channel' command/handler that loads the channel and unconditionally calls SetPrivate.

Common situations: A single 'UpdateChannel' API endpoint that reuses SetPrivate for all channel kinds; UI toggles that don't hide the privacy switch for DMs; seeded/imported data where the channel type changed after creation; handlers that skip a type check before mutating.

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/551d7757dcd1d682. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs:165

    public void Rename(string name, string? description)
    {
        if (Type != ChannelType.Channel)
        {
            throw new InvalidOperationException("Only named Channels can be renamed.");
        }
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        Name = name.Trim();
        Slug = Slugify(name);
        Description = description?.Trim();
        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);

View on GitHub (pinned to 3f2959e683)