{"record":{"id":"02840a68f9e302ff","repo":"fullstackhero/dotnet-starter-kit","slug":"parent-message-belongs-to-a-different-channel","errorCode":null,"errorMessage":"Parent message belongs to a different channel.","messagePattern":"Parent message belongs to a different channel\\.","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"warning","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs","lineNumber":49,"sourceCode":"        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == cmd.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Channel not found.\");\n\n        channel.RequireMember(currentUserId);\n\n        Message? parent = null;\n        if (cmd.ParentMessageId is { } parentId)\n        {\n            parent = await db.Messages.FirstOrDefaultAsync(m => m.Id == parentId, cancellationToken)\n                .ConfigureAwait(false)\n                ?? throw new NotFoundException(\"Parent message not found.\");\n            if (parent.ChannelId != channel.Id)\n            {\n                throw new CustomException(\"Parent message belongs to a different channel.\", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);\n            }\n            if (parent.ParentMessageId.HasValue)\n            {\n                // 1-level deep only per spec.\n                throw new CustomException(\"Cannot reply to a reply — threads are single-level only.\", (IEnumerable<string>?)null, HttpStatusCode.BadRequest);\n            }\n        }\n\n        // Parse @username tokens to user ids; self-mentions and unresolved tokens are dropped silently.\n        // Only resolved *other* users attach as MessageMention rows + fire an event. Body may be empty.\n        var rawMatches = MentionParser.Parse(cmd.Body ?? string.Empty);\n        var distinctNames = rawMatches.Select(m => m.Username)\n            .Distinct(StringComparer.OrdinalIgnoreCase)\n            .ToArray();\n        var resolved = distinctNames.Length == 0\n            ? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n            : (Dictionary<string, string>)await mentionResolver\n                .ResolveUserIdsAsync(distinctNames, cancellationToken)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs#L31-L67","documentation":"A reply's ParentMessageId must reference a message in the same channel. The handler compares parent.ChannelId to the target channel and throws a 400 CustomException(\"Parent message belongs to a different channel.\") on mismatch, preventing cross-channel threads.","triggerScenarios":"Submitting a reply where ParentMessageId points at a message from another channel than cmd.ChannelId — typically from buggy client state or an API consumer hand-assembling the request.","commonSituations":"UI switched channels but kept the reply draft and its parentId, deep-link/share copied a reply across channels, or automated scripts reusing ids.","solutions":["Reset reply draft state whenever the active channel changes in the UI.","Before sending, assert parent.channelId === channelId client-side.","If calling the API directly, fetch the parent message and confirm its channel matches.","Return the user to the parent's channel if cross-channel reply was intended."],"exampleFix":"// before\nsendMessage({ channelId: activeChannelId, parentId: draft.parentId, body });\n// after\nif (draft.channelId !== activeChannelId) clearReplyDraft();\nelse sendMessage({ channelId: activeChannelId, parentId: draft.parentId, body });","handlingStrategy":"validation","validationCode":"const parent = messages.find(m => m.id === parentId);\nif (parent && parent.channelId !== channelId) throw new Error('Reply must target a message in the same channel');","typeGuard":null,"tryCatchPattern":"try { await sendReply(cmd); }\ncatch (e) { if (e.status === 400 && /different channel/.test(e.message)) { clearReplyDraft(); notify('Reply draft reset'); } else throw e; }","preventionTips":["Clear reply drafts on channel switch","Bind the draft to its channel id and compare before send"],"tags":["chat","threads","bad-request"],"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"}