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
- Check message.DeletedAtUtc before calling Pin and hide/disable the pin action for deleted messages
- Catch InvalidOperationException (map to HTTP 409 at the endpoint) and inform the user the message no longer exists
- 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
- Hide pin action for soft-deleted messages
- Subscribe to message-deleted events to update UI
- Handle 409 Conflict from the endpoint gracefully
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
- Cannot react to 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/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)