{"record":{"id":"66af8030efe4a890","repo":"fullstackhero/dotnet-starter-kit","slug":"only-the-author-can-edit-a-message","errorCode":null,"errorMessage":"Only the author can edit a message.","messagePattern":"Only the author can edit a message\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Modules/Chat/Modules.Chat/Domain/Message.cs","lineNumber":87,"sourceCode":"            new MessageCreatedDomainEvent(channelId, m.Id, authorUserId, parentMessageId, id, ts)));\n        return m;\n    }\n\n    /// <summary>\n    /// Input shape for <see cref=\"Create\"/>: a resolved mention with the original position range\n    /// in the body so the UI can render the highlight without re-parsing.\n    /// </summary>\n    public readonly record struct ParsedMention(string MentionedUserId, int StartIndex, int Length);\n\n    public void Edit(string newBody, string editingUserId)\n    {\n        if (DeletedAtUtc.HasValue)\n        {\n            throw new InvalidOperationException(\"Cannot edit a deleted message.\");\n        }\n        if (!string.Equals(AuthorUserId, editingUserId, StringComparison.Ordinal))\n        {\n            throw new InvalidOperationException(\"Only the author can edit a message.\");\n        }\n        ArgumentException.ThrowIfNullOrWhiteSpace(newBody);\n\n        Body = newBody.Trim();\n        EditedAtUtc = DateTime.UtcNow;\n        AddDomainEvent(DomainEvent.Create((id, ts) =>\n            new MessageEditedDomainEvent(ChannelId, Id, AuthorUserId, id, ts)));\n    }\n\n    public void SoftDelete(string deletingUserId, bool isModerator)\n    {\n        if (DeletedAtUtc.HasValue) return;\n        if (!isModerator && !string.Equals(AuthorUserId, deletingUserId, StringComparison.Ordinal))\n        {\n            throw new InvalidOperationException(\"Only the author or a moderator can delete.\");\n        }\n        DeletedAtUtc = DateTime.UtcNow;\n        Body = null;","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Chat/Modules.Chat/Domain/Message.cs#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check message.AuthorUserId == currentUserId (or a domain-level CanEdit) before calling Edit and return 403 otherwise.","Catch InvalidOperationException in the handler and translate it to a 403 Forbidden response rather than 500.","For moderation edits, add an explicit domain operation (e.g. ModerateEdit) instead of bypassing the author check.","Normalize user id casing/format when creating messages so ordinal comparison matches."],"exampleFix":"// before\nmessage.Edit(request.NewBody, currentUserId);\n// after\nif (message.AuthorUserId != currentUserId)\n{\n    throw new ForbiddenAccessException(\"Only the author can edit a message.\");\n}\nmessage.Edit(request.NewBody, currentUserId);","handlingStrategy":"validation","validationCode":"public static bool CanEdit(Domain.Message m, string userId) => m.DeletedAtUtc is null && string.Equals(m.AuthorUserId, userId, StringComparison.Ordinal);","typeGuard":"if (message.AuthorUserId != currentUserId) throw new ForbiddenAccessException(\"Only the author can edit a message.\");","tryCatchPattern":"try { message.Edit(newBody, userId); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"Only the author\")) { throw new ForbiddenAccessException(ex.Message); }","preventionTips":["Check authorship in the endpoint/handler before invoking the aggregate","Return 403 (not 500) for ownership violations","Don't reuse Edit for moderator flows — add a dedicated operation","Keep user ids stored in a canonical format"],"tags":["domain","chat","edit","authorization","ownership"],"backgroundTag":"permission-denied","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"}