{"record":{"id":"acdce83a03ed66ea","repo":"fullstackhero/dotnet-starter-kit","slug":"message-not-found","errorCode":null,"errorMessage":"Message not found.","messagePattern":"Message not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Features/v1/Messages/DeleteMessage/DeleteMessageCommandHandler.cs","lineNumber":31,"sourceCode":"namespace FSH.Modules.Chat.Features.v1.Messages.DeleteMessage;\n\npublic sealed class DeleteMessageCommandHandler(\n    ChatDbContext db,\n    ICurrentUser currentUser,\n    IUserPermissionService permissions,\n    IHubContext<AppHub> hub)\n    : ICommandHandler<DeleteMessageCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteMessageCommand 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 message = await db.Messages.FirstOrDefaultAsync(m => m.Id == cmd.MessageId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Message not found.\");\n\n        var channel = await db.Channels.FirstOrDefaultAsync(c => c.Id == message.ChannelId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Message not found.\");\n        channel.RequireMember(currentUserId);\n\n        bool isModerator = await permissions\n            .HasPermissionAsync(currentUserId, ChatPermissions.Messages.DeleteAny, cancellationToken)\n            .ConfigureAwait(false);\n\n        message.SoftDelete(currentUserId, isModerator);\n\n        // If this was a thread reply, decrement the parent's ReplyCount.\n        if (message.ParentMessageId is { } parentId)\n        {\n            var parent = await db.Messages.FirstOrDefaultAsync(m => m.Id == parentId, cancellationToken)\n                .ConfigureAwait(false);\n            parent?.DecrementReplyCount();","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Features/v1/Messages/DeleteMessage/DeleteMessageCommandHandler.cs#L13-L49","documentation":"DeleteMessageCommandHandler throws NotFoundException(\"Message not found.\") when the message row itself is missing (FirstOrDefaultAsync on db.Messages by cmd.MessageId returns null). Standard 404 for a nonexistent aggregate. The tenant query filter can also hide an existing message belonging to another tenant.","triggerScenarios":"DELETE /messages/{id} with an id that was already deleted, never existed, or is not visible in the current tenant's query scope.","commonSituations":"Double-clicking delete so the second call targets an already-deleted message; stale client cache holding removed messages; cross-tenant id collision in tests; message hard-deleted by a moderator while the user was viewing it.","solutions":["Check the message exists in the current tenant before deleting; treat 404 on a second delete as success (idempotency).","Refresh the message list on the client after any delete so stale ids are not reused.","Verify the tenant context of the request matches the message's tenant.","Confirm migrations/seed data if the message should exist."],"exampleFix":"// before\nawait deleteMessage(messageId); // second click throws 404\n\n// after\ntry { await deleteMessage(messageId); }\ncatch (NotFoundException) { /* already deleted — treat as success */ }","handlingStrategy":"validation","validationCode":"const msg = messagesQuery.data?.find(m => m.id === messageId);\nif (!msg) return; // already gone — skip delete","typeGuard":null,"tryCatchPattern":"try {\n  await api.deleteMessage(messageId);\n} catch (e) {\n  if (e.status === 404) { removeLocal(messageId); /* idempotent success */ }\n  else throw e;\n}","preventionTips":["Treat delete 404 as idempotent success in clients.","Invalidate message queries immediately after delete.","Avoid reusing message ids stored in local state across sessions.","Verify tenant context before assuming the message should exist."],"tags":["chat","not-found","message","ef-core"],"backgroundTag":"entity-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"}