{"record":{"id":"6a85ae45bdacf0b9","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-post-a-comment-without-an-authenticated-author","errorCode":null,"errorMessage":"Cannot post a comment without an authenticated author.","messagePattern":"Cannot post a comment without an authenticated author\\.","errorType":"exception","errorClass":"CustomException","httpStatus":401,"severity":"error","filePath":"src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/AddTicketComment/AddTicketCommentCommandHandler.cs","lineNumber":23,"sourceCode":"using FSH.Modules.Tickets.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Tickets.Features.v1.Tickets.AddTicketComment;\n\npublic sealed class AddTicketCommentCommandHandler(\n    TicketsDbContext dbContext,\n    ICurrentUser currentUser)\n    : ICommandHandler<AddTicketCommentCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(AddTicketCommentCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var authorId = currentUser.GetUserId();\n        if (authorId == Guid.Empty)\n        {\n            throw new CustomException(\n                \"Cannot post a comment without an authenticated author.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Unauthorized);\n        }\n\n        // Load the Comments collection up front so EF's change tracker detects the new TicketComment\n        // (added via the aggregate) as an INSERT rather than missing it during change detection.\n        var ticket = await dbContext.Tickets\n            .Include(t => t.Comments)\n            .FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Ticket {command.TicketId} not found.\");\n\n        var commentId = ticket.AddComment(authorId, command.Body);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return commentId;\n    }\n}","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/AddTicketComment/AddTicketCommentCommandHandler.cs#L5-L41","documentation":"AddTicketCommentCommandHandler resolves the current user via currentUser.GetUserId(); if it returns Guid.Empty (no authenticated principal), the handler throws a 401 CustomException because a comment cannot be attributed to an author. The guard protects comment auditability.","triggerScenarios":"POSTing a ticket comment with no/invalid JWT, an expired token stripped by auth middleware, or an anonymous endpoint invocation where ICurrentUser.GetUserId() yields Guid.Empty.","commonSituations":"Calling the API without the Authorization header; token expired client-side but request still routed; integration tests invoking the handler without a mocked ICurrentUser returning a real user id.","solutions":["Ensure the request carries a valid Bearer token before calling the add-comment endpoint.","Refresh an expired JWT and retry the request.","Verify the endpoint requires authorization ([Authorize] / permission gate) so anonymous calls are rejected earlier.","In tests, configure ICurrentUser.GetUserId() to return a non-empty Guid."],"exampleFix":"// before\nawait http.PostAsync($\"/tickets/v1/{id}/comments\", content); // no auth header\n// after\nrequest.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\nawait http.SendAsync(request);","handlingStrategy":"validation","validationCode":"if (currentUser.GetUserId() == Guid.Empty)\n    throw new UnauthorizedAccessException(\"Sign in before posting a comment.\");","typeGuard":"static bool IsAuthenticated(ICurrentUser user) => user.GetUserId() != Guid.Empty;","tryCatchPattern":"try { await mediator.Send(new AddTicketCommentCommand { TicketId = id, Body = body }); }\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { await refreshTokenAndRetry(); }","preventionTips":["Always attach the Bearer token via an API-client interceptor.","Handle 401 globally by refreshing the token once and replaying the request.","Require authorization on comment endpoints so anonymous calls fail at the gate.","In tests, stub ICurrentUser with a real user id."],"tags":["auth","tickets","comments","unauthorized"],"backgroundTag":"authentication-required","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"}