fullstackhero/dotnet-starter-kit · error · InvalidOperationException

Cannot pin a deleted message.

Error message

Cannot pin a deleted message.

What it means

Chat Message.Pin throws this InvalidOperationException when the message has been soft-deleted (DeletedAtUtc is set). Pinning preserves a message for the channel, which is meaningless and disallowed for a deleted message, so the domain blocks it up front.

Solutions

  1. Check message.DeletedAtUtc before calling Pin and hide/disable the pin action for deleted messages
  2. Catch InvalidOperationException (map to HTTP 409 at the endpoint) and inform the user the message no longer exists
  3. Listen for message-deleted events and remove pin affordances in real time

Example fix

// before
message.Pin(userId); // throws if deleted
// after
if (message.DeletedAtUtc is null)
{
    message.Pin(userId);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (message.DeletedAtUtc is not null) return; // skip pin

Type guard

bool canPin = message is { DeletedAtUtc: null };

Try / catch

try { message.Pin(userId); }
catch (InvalidOperationException) { /* message deleted — notify user */ }

Prevention

When it happens

Trigger: Calling message.Pin(pinningUserId) on a message with DeletedAtUtc.HasValue — e.g. pinning from a stale UI list after someone deleted the message, or an automated moderation flow racing a delete.

Common situations: Moderator pins a message that was deleted milliseconds earlier by the author; stale channel history view; bulk admin scripts operating on cached message IDs.

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/1d65f2f902aae0ad. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Chat/Modules.Chat/Domain/Message.cs:172

        return true;
    }

    internal void IncrementReplyCount() => ReplyCount++;

    internal void DecrementReplyCount() => ReplyCount = Math.Max(0, ReplyCount - 1);

    /// <summary>
    /// Pin the message to its channel. Idempotent — re-pinning by a different user updates the
    /// PinnedByUserId / PinnedAtUtc stamp but doesn't produce a duplicate event. Pinning a
    /// soft-deleted message is rejected; pinning a reply is permitted (channels can pin a
    /// specific thread reply, e.g. an answer).
    /// </summary>
    public void Pin(string pinningUserId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(pinningUserId);
        if (DeletedAtUtc.HasValue)
        {
            throw new InvalidOperationException("Cannot pin a deleted message.");
        }
        if (IsPinned && string.Equals(PinnedByUserId, pinningUserId, StringComparison.Ordinal))
        {
            // Already pinned by this user — no-op, no event.
            return;
        }

        IsPinned = true;
        PinnedByUserId = pinningUserId;
        PinnedAtUtc = DateTime.UtcNow;
        AddDomainEvent(DomainEvent.Create((id, ts) =>
            new MessagePinnedDomainEvent(ChannelId, Id, pinningUserId, id, ts)));
    }

    /// <summary>
    /// Unpin the message. Idempotent — unpinning an already-unpinned message is a no-op.
    /// </summary>
    public void Unpin(string unpinningUserId)

View on GitHub (pinned to 3f2959e683)