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

  1. Check message.DeletedAtUtc before calling AddReaction and disable the reaction UI for deleted messages
  2. Catch InvalidOperationException (or map domain InvalidOperationException to HTTP 409 Conflict at the endpoint) and surface 'message was deleted' to the client
  3. 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

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


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)