{"record":{"id":"1d65f2f902aae0ad","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-pin-a-deleted-message","errorCode":null,"errorMessage":"Cannot pin a deleted message.","messagePattern":"Cannot pin a deleted message\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/Message.cs","lineNumber":172,"sourceCode":"        return true;\n    }\n\n    internal void IncrementReplyCount() => ReplyCount++;\n\n    internal void DecrementReplyCount() => ReplyCount = Math.Max(0, ReplyCount - 1);\n\n    /// <summary>\n    /// Pin the message to its channel. Idempotent — re-pinning by a different user updates the\n    /// PinnedByUserId / PinnedAtUtc stamp but doesn't produce a duplicate event. Pinning a\n    /// soft-deleted message is rejected; pinning a reply is permitted (channels can pin a\n    /// specific thread reply, e.g. an answer).\n    /// </summary>\n    public void Pin(string pinningUserId)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(pinningUserId);\n        if (DeletedAtUtc.HasValue)\n        {\n            throw new InvalidOperationException(\"Cannot pin a deleted message.\");\n        }\n        if (IsPinned && string.Equals(PinnedByUserId, pinningUserId, StringComparison.Ordinal))\n        {\n            // Already pinned by this user — no-op, no event.\n            return;\n        }\n\n        IsPinned = true;\n        PinnedByUserId = pinningUserId;\n        PinnedAtUtc = DateTime.UtcNow;\n        AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new MessagePinnedDomainEvent(ChannelId, Id, pinningUserId, id, ts)));\n    }\n\n    /// <summary>\n    /// Unpin the message. Idempotent — unpinning an already-unpinned message is a no-op.\n    /// </summary>\n    public void Unpin(string unpinningUserId)","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/Message.cs#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"Moderator pins a message that was deleted milliseconds earlier by the author; stale channel history view; bulk admin scripts operating on cached message IDs.","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"],"exampleFix":"// before\nmessage.Pin(userId); // throws if deleted\n// after\nif (message.DeletedAtUtc is null)\n{\n    message.Pin(userId);\n}","handlingStrategy":"try-catch","validationCode":"if (message.DeletedAtUtc is not null) return; // skip pin","typeGuard":"bool canPin = message is { DeletedAtUtc: null };","tryCatchPattern":"try { message.Pin(userId); }\ncatch (InvalidOperationException) { /* message deleted — notify user */ }","preventionTips":["Hide pin action for soft-deleted messages","Subscribe to message-deleted events to update UI","Handle 409 Conflict from the endpoint gracefully"],"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"}