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
- Check Channel.Type before calling SetPrivate and skip the call when Type != ChannelType.Channel (DMs have no privacy concept).
- 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.
- Restrict the API contract so the isPrivate flag is only accepted for Channel-type channels (conditional validation in the FluentValidation validator).
- 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
- Only expose the privacy toggle in the UI for Channel-type channels
- Add a type-conditional FluentValidation rule on the update command
- Translate domain InvalidOperationExceptions into 4xx responses in handlers
- Never call SetPrivate blindly in generic update handlers
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
- Direct messages have fixed membership.
- Cannot edit a deleted message.
- Cannot mark invoice as paid from status
- Paid invoices cannot be voided.
- Operation requires invoice status
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)