fullstackhero/dotnet-starter-kit · error · InvalidOperationException

Only the author can edit a message.

Error message

Only the author can edit a message.

What it means

Message.Edit enforces authorship: only the user whose id equals AuthorUserId (ordinal comparison) may change the body. Any other caller — even a moderator, unlike SoftDelete — throws InvalidOperationException. This is an ownership guard, so authorization must not be delegated to the aggregate.

Solutions

  1. Check message.AuthorUserId == currentUserId (or a domain-level CanEdit) before calling Edit and return 403 otherwise.
  2. Catch InvalidOperationException in the handler and translate it to a 403 Forbidden response rather than 500.
  3. For moderation edits, add an explicit domain operation (e.g. ModerateEdit) instead of bypassing the author check.
  4. Normalize user id casing/format when creating messages so ordinal comparison matches.

Example fix

// before
message.Edit(request.NewBody, currentUserId);
// after
if (message.AuthorUserId != currentUserId)
{
    throw new ForbiddenAccessException("Only the author can edit a message.");
}
message.Edit(request.NewBody, currentUserId);
Defensive patterns

Strategy: validation

Validate before calling

public static bool CanEdit(Domain.Message m, string userId) => m.DeletedAtUtc is null && string.Equals(m.AuthorUserId, userId, StringComparison.Ordinal);

Type guard

if (message.AuthorUserId != currentUserId) throw new ForbiddenAccessException("Only the author can edit a message.");

Try / catch

try { message.Edit(newBody, userId); } catch (InvalidOperationException ex) when (ex.Message.Contains("Only the author")) { throw new ForbiddenAccessException(ex.Message); }

Prevention

When it happens

Trigger: Calling message.Edit(newBody, editingUserId) where editingUserId != message.AuthorUserId — e.g. a handler passing the current user's id but the client targeted another user's message, or an admin endpoint reusing Edit.

Common situations: Missing authorization check in the endpoint layer letting users edit others' messages (caught here as 500 instead of 403); admin 'edit any message' tooling incorrectly calling Edit; user id casing/format mismatches between token and stored AuthorUserId.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/66af8030efe4a890. Report an issue: GitHub.

Appendix: source

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

            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))
        {
            throw new InvalidOperationException("Only the author or a moderator can delete.");
        }
        DeletedAtUtc = DateTime.UtcNow;
        Body = null;

View on GitHub (pinned to 3f2959e683)