{"record":{"id":"daf7f10626b286cb","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-react-to-a-deleted-message","errorCode":null,"errorMessage":"Cannot react to a deleted message.","messagePattern":"Cannot react to a deleted message\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/Message.cs","lineNumber":127,"sourceCode":"\n    public MessageAttachment AddAttachment(Guid? fileAssetId, string url, string contentType, string fileName, long sizeBytes)\n    {\n        var attachment = MessageAttachment.Create(Id, fileAssetId, url, contentType, fileName, sizeBytes);\n        _attachments.Add(attachment);\n        return attachment;\n    }\n\n    /// <summary>\n    /// Toggle-on a reaction. Returns the new <see cref=\"MessageReaction\"/>, or <c>null</c> if the\n    /// (user, emoji) pair already exists — the unique index would reject the duplicate row.\n    /// </summary>\n    public MessageReaction? AddReaction(string userId, string emoji)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(userId);\n        ArgumentException.ThrowIfNullOrWhiteSpace(emoji);\n        if (DeletedAtUtc.HasValue)\n        {\n            throw new InvalidOperationException(\"Cannot react to a deleted message.\");\n        }\n        var trimmed = emoji.Trim();\n        if (_reactions.Any(r => string.Equals(r.UserId, userId, StringComparison.Ordinal)\n                             && string.Equals(r.Emoji, trimmed, StringComparison.Ordinal)))\n        {\n            return null;\n        }\n        var reaction = MessageReaction.Create(Id, userId, trimmed);\n        _reactions.Add(reaction);\n        return reaction;\n    }\n\n    /// <summary>\n    /// Toggle-off a reaction. Returns <c>true</c> if a row was removed; <c>false</c> if the user\n    /// hadn't reacted with that emoji.\n    /// </summary>\n    public bool RemoveReaction(string userId, string emoji)\n    {","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/Message.cs#L109-L145","documentation":"Chat Message.AddReaction throws this InvalidOperationException when the target message has been soft-deleted (DeletedAtUtc is set). Deleted messages are immutable by design — the domain forbids attaching new reactions so audit/history stays consistent. Callers must treat any operation on a deleted message as invalid.","triggerScenarios":"Calling message.AddReaction(userId, emoji) on a message whose DeletedAtUtc.HasValue is true — e.g. reacting to a message another user just deleted, or reacting from a stale client view that still shows the message.","commonSituations":"Real-time race: message deleted via another session/SignalR event between render and click; stale cached message list; double-submit after delete; tests reusing a deleted fixture message.","solutions":["Check message.DeletedAtUtc before calling AddReaction and disable the reaction UI for deleted messages","Catch InvalidOperationException (or map domain InvalidOperationException to HTTP 409 Conflict at the endpoint) and surface 'message was deleted' to the client","Update the client message state on delete events so stale messages cannot be interacted with"],"exampleFix":"// before\nmessage.AddReaction(userId, emoji); // throws if deleted\n// after\nif (message.DeletedAtUtc is null)\n{\n    message.AddReaction(userId, emoji);\n}","handlingStrategy":"try-catch","validationCode":"if (message.DeletedAtUtc is not null) throw/return; // check before AddReaction","typeGuard":"bool canReact = message is { DeletedAtUtc: null };","tryCatchPattern":"try { message.AddReaction(userId, emoji); }\ncatch (InvalidOperationException) { /* message was deleted — refresh and inform user */ }","preventionTips":["Disable reaction UI on deleted messages","Keep client message state in sync with delete events","Map domain InvalidOperationException to 409 at the endpoint boundary"],"tags":["chat","domain-rule","deleted-entity","invalid-state"],"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"}