fullstackhero/dotnet-starter-kit · error · InvalidOperationException
Cannot react to a deleted message.
Error message
Cannot react to a deleted message.
What it means
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.
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
Example fix
// before
message.AddReaction(userId, emoji); // throws if deleted
// after
if (message.DeletedAtUtc is null)
{
message.AddReaction(userId, emoji);
} Defensive patterns
Strategy: try-catch
Validate before calling
if (message.DeletedAtUtc is not null) throw/return; // check before AddReaction
Type guard
bool canReact = message is { DeletedAtUtc: null }; Try / catch
try { message.AddReaction(userId, emoji); }
catch (InvalidOperationException) { /* message was deleted — refresh and inform user */ } Prevention
- Disable reaction UI on deleted messages
- Keep client message state in sync with delete events
- Map domain InvalidOperationException to 409 at the endpoint boundary
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot pin a deleted message.
- Only named Channels can be renamed.
- Cannot start a DM with yourself.
- Group DM requires at least 3 distinct members.
- All user ids must be non-empty.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/daf7f10626b286cb.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Chat/Modules.Chat/Domain/Message.cs:127
public MessageAttachment AddAttachment(Guid? fileAssetId, string url, string contentType, string fileName, long sizeBytes)
{
var attachment = MessageAttachment.Create(Id, fileAssetId, url, contentType, fileName, sizeBytes);
_attachments.Add(attachment);
return attachment;
}
/// <summary>
/// Toggle-on a reaction. Returns the new <see cref="MessageReaction"/>, or <c>null</c> if the
/// (user, emoji) pair already exists — the unique index would reject the duplicate row.
/// </summary>
public MessageReaction? AddReaction(string userId, string emoji)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userId);
ArgumentException.ThrowIfNullOrWhiteSpace(emoji);
if (DeletedAtUtc.HasValue)
{
throw new InvalidOperationException("Cannot react to a deleted message.");
}
var trimmed = emoji.Trim();
if (_reactions.Any(r => string.Equals(r.UserId, userId, StringComparison.Ordinal)
&& string.Equals(r.Emoji, trimmed, StringComparison.Ordinal)))
{
return null;
}
var reaction = MessageReaction.Create(Id, userId, trimmed);
_reactions.Add(reaction);
return reaction;
}
/// <summary>
/// Toggle-off a reaction. Returns <c>true</c> if a row was removed; <c>false</c> if the user
/// hadn't reacted with that emoji.
/// </summary>
public bool RemoveReaction(string userId, string emoji)
{View on GitHub (pinned to 3f2959e683)