{"record":{"id":"9373e17709c76cfa","repo":"kgrzybek/modular-monolith-with-ddd","slug":"meeting-comment-like-for-removing-must-exist","errorCode":null,"errorMessage":"Meeting comment like for removing must exist.","messagePattern":"Meeting comment like for removing must exist\\.","errorType":"validation","errorClass":"InvalidCommandException","httpStatus":400,"severity":"error","filePath":"src/Modules/Meetings/Application/MeetingComments/RemoveMeetingCommentLike/RemoveMeetingCommentLikeCommandHandler.cs","lineNumber":25,"sourceCode":"namespace CompanyName.MyMeetings.Modules.Meetings.Application.MeetingComments.RemoveMeetingCommentLike\r\n{\r\n    internal class RemoveMeetingCommentLikeCommandHandler : ICommandHandler<RemoveMeetingCommentLikeCommand>\r\n    {\r\n        private readonly IMeetingMemberCommentLikesRepository _meetingMemberCommentLikesRepository;\r\n        private readonly IMemberContext _memberContext;\r\n\r\n        internal RemoveMeetingCommentLikeCommandHandler(IMeetingMemberCommentLikesRepository meetingMemberCommentLikesRepository, IMemberContext memberContext)\r\n        {\r\n            _meetingMemberCommentLikesRepository = meetingMemberCommentLikesRepository;\r\n            _memberContext = memberContext;\r\n        }\r\n\r\n        public async Task Handle(RemoveMeetingCommentLikeCommand command, CancellationToken cancellationToken)\r\n        {\r\n            var commentLike = await _meetingMemberCommentLikesRepository.GetAsync(_memberContext.MemberId, new MeetingCommentId(command.MeetingCommentId));\r\n            if (commentLike == null)\r\n            {\r\n                throw new InvalidCommandException([\"Meeting comment like for removing must exist.\"]);\r\n            }\r\n\r\n            commentLike.Remove();\r\n\r\n            _meetingMemberCommentLikesRepository.Remove(commentLike);\r\n        }\r\n    }\r\n}","sourceCodeStart":7,"sourceCodeEnd":33,"githubUrl":"https://github.com/kgrzybek/modular-monolith-with-ddd/blob/91c8ef24b4cb6ef558c95d8267fa07d68c7059f8/src/Modules/Meetings/Application/MeetingComments/RemoveMeetingCommentLike/RemoveMeetingCommentLikeCommandHandler.cs#L7-L33","documentation":"Thrown by RemoveMeetingCommentLikeCommandHandler when _meetingMemberCommentLikesRepository.GetAsync(memberId, commentId) returns null. A like can only be removed if it exists; a missing like record aborts the command with InvalidCommandException (HTTP 400).","triggerScenarios":"Dispatching RemoveMeetingCommentLikeCommand for a like that does not exist: the member never liked the comment, already unliked it, or the comment/like was removed.","commonSituations":"User toggles like off twice rapidly; client optimistic state out of sync; unlike after a server-side cascade removed likes.","solutions":["Make unlike idempotent: treat a missing like as success rather than an error.","Pre-check the like exists via a query before dispatching remove.","Synchronize client state so a second unlike is not sent.","Map InvalidCommandException to 400/404 at the API boundary."],"exampleFix":"// before\nawait _commandDispatcher.SendAsync(new RemoveMeetingCommentLikeCommand(commentId));\n\n// after\nvar liked = await _meetingQueries.IsLikedByMemberAsync(commentId, memberId);\nif (!liked) return NoContent(); // nothing to remove\nawait _commandDispatcher.SendAsync(new RemoveMeetingCommentLikeCommand(commentId));","handlingStrategy":"validation","validationCode":"var liked = await _meetingQueries.IsLikedByMemberAsync(commentId, memberId);\nif (!liked) return NoContent(); // nothing to remove\nawait _commandDispatcher.SendAsync(new RemoveMeetingCommentLikeCommand(commentId));","typeGuard":"public static bool IsValidUnlikeCommand(RemoveMeetingCommentLikeCommand c) =>\n    c.MeetingCommentId != Guid.Empty;","tryCatchPattern":"try { await _commandDispatcher.SendAsync(cmd); }\ncatch (InvalidCommandException ex) when (ex.Errors.Any(m => m.Contains(\"must exist\")))\n{ return NoContent(); } // idempotent unlike","preventionTips":["Make unlike idempotent: missing like is success, not an error.","Keep client like-state synchronized to avoid sending duplicate unlikes.","Guard empty Guid at the controller."],"tags":["meetings","comments","likes","not-found","invalid-command-exception","command-handler"],"backgroundTag":null,"analyzedSha":"91c8ef24b4cb6ef558c95d8267fa07d68c7059f8","analyzedAt":"2026-08-13T17:18:16.571Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}