fullstackhero/dotnet-starter-kit · error · NotFoundException
Notification not found.
Error message
Notification not found.
What it means
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.
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.
Example fix
// before
await mediator.Send(new MarkNotificationReadCommand(notificationId), ct); // 404 if stale
// after
try
{
await mediator.Send(new MarkNotificationReadCommand(notificationId), ct);
}
catch (NotFoundException)
{
queryClient.invalidateQueries(["notifications"]); // drop stale id, don't surface error
} Defensive patterns
Strategy: try-catch
Validate before calling
var owned = notifications.some(n => n.id === notificationId);
if (!owned) { console.warn('Notification id not in current user\'s list; skipping.'); return; } Type guard
bool IsOwnNotification(NotificationDto? n, string currentUserId) => n is not null && n.UserId == currentUserId;
Try / catch
try
{
await mediator.Send(new MarkNotificationReadCommand(notificationId), ct);
}
catch (NotFoundException)
{
// stale or foreign id — drop it locally, don't surface an error
queryClient.invalidateQueries(["notifications"]);
} Prevention
- 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.
When it happens
Trigger: Passing a NotificationId that does not exist, an already-deleted notification, an id from another tenant/user, or a malformed/duplicated client-side id.
Common situations: 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.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/9c9d69e779288044.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Notifications/Modules.Notifications/Features/v1/MarkNotificationRead/MarkNotificationReadCommandHandler.cs:27
public sealed class MarkNotificationReadCommandHandler(
NotificationsDbContext db,
ICurrentUser currentUser)
: ICommandHandler<MarkNotificationReadCommand, Unit>
{
public async ValueTask<Unit> Handle(MarkNotificationReadCommand cmd, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(cmd);
var userId = currentUser.GetUserId();
if (userId == Guid.Empty) throw new UnauthorizedException("no current user");
var currentUserId = userId.ToString();
// Caller-scoped: filter by (Id, UserId) so users can only mutate their own rows. Returns
// 404 if the row exists but belongs to someone else — we don't leak existence.
var notification = await db.Notifications
.FirstOrDefaultAsync(n => n.Id == cmd.NotificationId && n.UserId == currentUserId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException("Notification not found.");
notification.MarkRead();
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
View on GitHub (pinned to 3f2959e683)