{"record":{"id":"7ffa2656c03b7241","repo":"fullstackhero/dotnet-starter-kit","slug":"message-not-found-in-this-channel","errorCode":null,"errorMessage":"Message not found in this channel.","messagePattern":"Message not found in this channel\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"warning","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Channels/MarkChannelRead/MarkChannelReadCommandHandler.cs","lineNumber":35,"sourceCode":"    : ICommandHandler<MarkChannelReadCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(MarkChannelReadCommand 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        channel.RequireMember(currentUserId);\n\n        // Verify the marker message actually exists in this channel.\n        var exists = await db.Messages\n            .AnyAsync(m => m.Id == cmd.MessageId && m.ChannelId == cmd.ChannelId, cancellationToken)\n            .ConfigureAwait(false);\n        if (!exists) throw new NotFoundException(\"Message not found in this channel.\");\n\n        channel.MarkRead(currentUserId, cmd.MessageId);\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n\n        // Push to the user's other tabs so the badge clears everywhere at once.\n        await hub.Clients.Group($\"user:{currentUserId}\")\n            .SendAsync(\"ChatChannelRead\",\n                new { channelId = cmd.ChannelId, lastReadMessageId = cmd.MessageId },\n                cancellationToken)\n            .ConfigureAwait(false);\n\n        // Push to the channel group so members update read receipts in real time. The reader's own\n        // connections also receive this (alongside the user-scoped push); the client handler is idempotent.\n        await hub.Clients.Group($\"channel:{cmd.ChannelId}\")\n            .SendAsync(\"ChatChannelMemberRead\",\n                new { channelId = cmd.ChannelId, userId = currentUserId, lastReadMessageId = cmd.MessageId },\n                cancellationToken)\n            .ConfigureAwait(false);","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Channels/MarkChannelRead/MarkChannelReadCommandHandler.cs#L17-L53","documentation":"MarkChannelReadCommandHandler verifies the marker message exists in the target channel via db.Messages.AnyAsync(m => m.Id == cmd.MessageId && m.ChannelId == cmd.ChannelId); if not, it throws NotFoundException. The message id must reference a message that both exists AND belongs to the same channel.","triggerScenarios":"Marking read with a messageId from a different channel; the message was hard/soft deleted; a client passed the channel id instead of the message id; race where the message was deleted between display and mark-read.","commonSituations":"Mixed-up ids in UI code (lastMessageId vs channelId swap); stale unread pointers after message deletion; optimistic UI clearing a badge for a message that no longer exists; replayed queued read events.","solutions":["Send the id of the last message actually present in that channel.","Re-fetch the channel's latest message id if unsure.","Skip/ignore 404 from mark-read — it's safe to clear local unread state anyway.","Add a channel-deleted/message-deleted handler that resets local unread pointers."],"exampleFix":"// before\nmarkRead(channel.id, lastVisibleMessage.channelId); // wrong field\n// after\nmarkRead(channel.id, lastVisibleMessage.id); // must be the message's own id in this channel","handlingStrategy":"validation","validationCode":"if (!messageId || !lastMessageInChannel(channelId, messageId)) {\n  messageId = await fetchLatestMessageId(channelId);\n}","typeGuard":"const isValidMarker = (m: {id: string; channelId: string} | null, channelId: string): m is {id: string; channelId: string} =>\n  !!m && m.channelId === channelId && typeof m.id === 'string';","tryCatchPattern":"try { ... } catch (e) {\n  if (isNotFound(e)) { clearLocalUnread(channelId); } // message gone; safe to clear\n  else throw e;\n}","preventionTips":["Send the message's own id, not the channel id — watch for swapped fields.","After message deletion, refresh the latest-message pointer before marking read.","Treat mark-read 404s as benign and clear local unread state.","Keep unread pointers in sync via real-time channel events."],"tags":["chat","not-found","read-receipt"],"backgroundTag":"resource-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"}