{"record":{"id":"9c9d69e779288044","repo":"fullstackhero/dotnet-starter-kit","slug":"notification-not-found","errorCode":null,"errorMessage":"Notification not found.","messagePattern":"Notification not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Notifications/Modules.Notifications/Features/v1/MarkNotificationRead/MarkNotificationReadCommandHandler.cs","lineNumber":27,"sourceCode":"\npublic sealed class MarkNotificationReadCommandHandler(\n    NotificationsDbContext db,\n    ICurrentUser currentUser)\n    : ICommandHandler<MarkNotificationReadCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(MarkNotificationReadCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n        var userId = currentUser.GetUserId();\n        if (userId == Guid.Empty) throw new UnauthorizedException(\"no current user\");\n        var currentUserId = userId.ToString();\n\n        // Caller-scoped: filter by (Id, UserId) so users can only mutate their own rows. Returns\n        // 404 if the row exists but belongs to someone else — we don't leak existence.\n        var notification = await db.Notifications\n            .FirstOrDefaultAsync(n => n.Id == cmd.NotificationId && n.UserId == currentUserId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"Notification not found.\");\n\n        notification.MarkRead();\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":9,"sourceCodeEnd":34,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Notifications/Modules.Notifications/Features/v1/MarkNotificationRead/MarkNotificationReadCommandHandler.cs#L9-L34","documentation":"MarkNotificationReadCommandHandler queries the notification by the pair (cmd.NotificationId, currentUserId) and throws NotFoundException(\"Notification not found.\") when no row matches. Because the query filters by the caller's UserId, this is also returned when the notification exists but belongs to another user — existence is deliberately not leaked. Maps to HTTP 404.","triggerScenarios":"Passing a NotificationId that does not exist, an already-deleted notification, an id from another tenant/user, or a malformed/duplicated client-side id.","commonSituations":"Client cached a notification id that was later deleted; user A's session tries to mark user B's notification (id copied across accounts); stale list data after another device marked/deleted it; wrong environment's database.","solutions":["Verify the NotificationId comes from a list query for the same authenticated user.","Handle 404 gracefully in the client: remove the item from the local list and show a non-blocking message.","Check the notification still exists in the DB: SELECT * FROM \"Notifications\" WHERE \"Id\" = '<id>' AND \"UserId\" = '<userId>'.","Re-sync the notification list before retrying, and don't retry blindly on 404."],"exampleFix":"// before\nawait mediator.Send(new MarkNotificationReadCommand(notificationId), ct); // 404 if stale\n// after\ntry\n{\n    await mediator.Send(new MarkNotificationReadCommand(notificationId), ct);\n}\ncatch (NotFoundException)\n{\n    queryClient.invalidateQueries([\"notifications\"]); // drop stale id, don't surface error\n}","handlingStrategy":"try-catch","validationCode":"var owned = notifications.some(n => n.id === notificationId);\nif (!owned) { console.warn('Notification id not in current user\\'s list; skipping.'); return; }","typeGuard":"bool IsOwnNotification(NotificationDto? n, string currentUserId) => n is not null && n.UserId == currentUserId;","tryCatchPattern":"try\n{\n    await mediator.Send(new MarkNotificationReadCommand(notificationId), ct);\n}\ncatch (NotFoundException)\n{\n    // stale or foreign id — drop it locally, don't surface an error\n    queryClient.invalidateQueries([\"notifications\"]);\n}","preventionTips":["Only pass notification ids obtained from the same user's list query.","Treat 404 as 'remove from local list', not as a user-facing error.","Re-sync the list after multi-device sessions before acting on cached ids.","Never retry mark-read on 404 — the row will not appear."],"tags":["not-found","notifications","ownership"],"backgroundTag":"record-not-found","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"}