{"record":{"id":"b7de9d215d907cad","repo":"fullstackhero/dotnet-starter-kit","slug":"parent-message-not-found-sendmessagecommandhandler","errorCode":null,"errorMessage":"Parent message not found.","messagePattern":"Parent message not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs","lineNumber":46,"sourceCode":"    public async ValueTask<MessageDto> Handle(SendMessageCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        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","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Messages/SendMessage/SendMessageCommandHandler.cs#L28-L64","documentation":"When SendMessageCommand carries a ParentMessageId, the handler loads that message and throws NotFoundException(\"Parent message not found.\") if it does not exist. Threads are modeled by pointing a message at its parent, so a dangling parent id is rejected up front.","triggerScenarios":"Replying to a message id that was deleted, never existed, or belongs to another tenant (hidden by the global query filter).","commonSituations":"Client kept a reply composer open after the parent message was deleted by a moderator, race between delete and reply submit, or copying a message id from another tenant/environment.","solutions":["Re-fetch the thread before submitting the reply and handle 404 by disabling the composer.","Refresh the message list after deletions (listen to the SignalR message-deleted event).","Verify the parent id belongs to the same channel/tenant as the target channel.","Check for typos or stale ids in the client state."],"exampleFix":"// before\nconst parent = messages.find(m => m.id === parentId); // may be deleted\nawait sendMessage({ channelId, parentId, body });\n// after\nif (!messages.some(m => m.id === parentId)) return; // parent gone, drop reply\nawait sendMessage({ channelId, parentId, body });","handlingStrategy":"validation","validationCode":"if (parentId && !messages.some(m => m.id === parentId)) throw new Error('Parent message no longer exists');","typeGuard":null,"tryCatchPattern":"try { await sendReply(cmd); }\ncatch (e) { if (e.status === 404) { closeReplyComposer(); refetchThread(); } else throw e; }","preventionTips":["Refresh threads on message-deleted events","Close reply composers whose parent disappears","Don't reuse ids copied from other environments"],"tags":["chat","threads","not-found"],"backgroundTag":"record-not-found","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"}