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
- Check message.DeletedAtUtc is null before calling Edit (or expose IsDeleted) and reject the edit with a friendly error.
- Catch InvalidOperationException in the handler and return 409 Conflict ('message already deleted').
- Reload the message inside the command handler so the deleted state is current; rely on concurrency tokens for the race with delete.
- 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
- Check DeletedAtUtc/IsDeleted before enabling edit in the UI
- Subscribe to MessageDeleted events to update client state
- Re-load the message in the handler before editing
- Disable edit actions on tombstoned messages
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
- Only named Channels can change privacy.
- Only the author can edit a message.
- Cannot mark invoice as paid from status
- Paid invoices cannot be voided.
- Operation requires invoice status
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)