fullstackhero/dotnet-starter-kit · error · InvalidOperationException

Cannot edit a deleted message.

Error message

Cannot edit a deleted message.

What it means

Message.Edit refuses to mutate a soft-deleted message: once DeletedAtUtc is set the message body is cleared and the aggregate is effectively a tombstone, so editing throws InvalidOperationException. Deleted state is terminal for edits in this domain model.

Solutions

  1. Check message.DeletedAtUtc is null before calling Edit (or expose IsDeleted) and reject the edit with a friendly error.
  2. Catch InvalidOperationException in the handler and return 409 Conflict ('message already deleted').
  3. Reload the message inside the command handler so the deleted state is current; rely on concurrency tokens for the race with delete.
  4. Update clients on MessageDeleted events so they disable editing for deleted messages.

Example fix

// before
message.Edit(request.NewBody, currentUserId);
// after
if (message.DeletedAtUtc is not null)
{
    throw new ConflictException("Cannot edit a deleted message.");
}
message.Edit(request.NewBody, currentUserId);
Defensive patterns

Strategy: validation

Validate before calling

public static bool CanEdit(Domain.Message m) => m.DeletedAtUtc is null;

Type guard

if (message is { DeletedAtUtc: not null }) throw new ConflictException("Cannot edit a deleted message.");

Try / catch

try { message.Edit(newBody, userId); } catch (InvalidOperationException ex) when (ex.Message.Contains("deleted")) { throw new ConflictException(ex.Message); }

Prevention

When it happens

Trigger: Calling message.Edit(newBody, editingUserId) on a message whose DeletedAtUtc is non-null — e.g. an edit command racing a concurrent delete, or a client editing a message it still sees cached after deletion.

Common situations: Two tabs open: one deletes, the other edits; stale client cache after a delete event that the client missed; handlers that don't re-load the message before editing; replayed/out-of-order integration events.

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/936c2517322a61a5. Report an issue: GitHub.

Appendix: source

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

                m._mentions.Add(MessageMention.Create(m.Id, pm.MentionedUserId, pm.StartIndex, pm.Length));
            }
        }
        m.AddDomainEvent(DomainEvent.Create((id, ts) =>
            new MessageCreatedDomainEvent(channelId, m.Id, authorUserId, parentMessageId, id, ts)));
        return m;
    }

    /// <summary>
    /// Input shape for <see cref="Create"/>: a resolved mention with the original position range
    /// in the body so the UI can render the highlight without re-parsing.
    /// </summary>
    public readonly record struct ParsedMention(string MentionedUserId, int StartIndex, int Length);

    public void Edit(string newBody, string editingUserId)
    {
        if (DeletedAtUtc.HasValue)
        {
            throw new InvalidOperationException("Cannot edit a deleted message.");
        }
        if (!string.Equals(AuthorUserId, editingUserId, StringComparison.Ordinal))
        {
            throw new InvalidOperationException("Only the author can edit a message.");
        }
        ArgumentException.ThrowIfNullOrWhiteSpace(newBody);

        Body = newBody.Trim();
        EditedAtUtc = DateTime.UtcNow;
        AddDomainEvent(DomainEvent.Create((id, ts) =>
            new MessageEditedDomainEvent(ChannelId, Id, AuthorUserId, id, ts)));
    }

    public void SoftDelete(string deletingUserId, bool isModerator)
    {
        if (DeletedAtUtc.HasValue) return;
        if (!isModerator && !string.Equals(AuthorUserId, deletingUserId, StringComparison.Ordinal))
        {

View on GitHub (pinned to 3f2959e683)