{"record":{"id":"551d7757dcd1d682","repo":"fullstackhero/dotnet-starter-kit","slug":"only-named-channels-can-change-privacy","errorCode":null,"errorMessage":"Only named Channels can change privacy.","messagePattern":"Only named Channels can change privacy\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs","lineNumber":165,"sourceCode":"\n    public void Rename(string name, string? description)\n    {\n        if (Type != ChannelType.Channel)\n        {\n            throw new InvalidOperationException(\"Only named Channels can be renamed.\");\n        }\n        ArgumentException.ThrowIfNullOrWhiteSpace(name);\n        Name = name.Trim();\n        Slug = Slugify(name);\n        Description = description?.Trim();\n        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);","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/ChatChannel.cs#L147-L183","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nchannel.SetPrivate(request.IsPrivate);\n// after\nif (channel.Type == ChannelType.Channel)\n{\n    channel.SetPrivate(request.IsPrivate);\n}\nelse if (request.IsPrivate != channel.IsPrivate)\n{\n    throw new InvalidOperationException(\"Only named Channels can change privacy.\");\n}","handlingStrategy":"validation","validationCode":"public static bool CanChangePrivacy(ChatChannel c) => c.Type == ChannelType.Channel;","typeGuard":"if (channel.Type != ChannelType.Channel) throw new ConflictException(\"Only named Channels can change privacy.\");","tryCatchPattern":"try { channel.SetPrivate(request.IsPrivate); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"privacy\")) { throw new ConflictException(ex.Message); }","preventionTips":["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"],"tags":["domain","chat","invalid-operation","state-machine"],"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"}