fullstackhero/dotnet-starter-kit · error · NotFoundException
Ticket not found.
Error message
Ticket {command.TicketId} not found. What it means
ResolveTicket's handler looks up the ticket by the command's TicketId and, when no matching row is returned, throws the app-wide NotFoundException with the message "Ticket {id} not found.". This is the standard FSH pattern: a CQRS command referencing a nonexistent or soft-deleted aggregate is surfaced as a typed not-found error (mapped to HTTP 404 by the exception middleware) rather than a NullReferenceException deeper in the pipeline.
Solutions
- Verify the TicketId exists with a query including soft-deleted rows (e.g. IgnoreQueryFilters) before/instead of resolving.
- If the ticket is soft-deleted and you intend to restore it, call the RestoreTicket endpoint instead of ResolveTicket.
- Check the request is going to the correct tenant (tenant header/route) so the tenant query filter matches the ticket's tenant.
- Confirm the id format: it must be the exact GUID of the ticket as returned by the list endpoint, not a number or external reference.
Example fix
// before (id from stale client cache)
var id = ticketList[0].Id; // ticket already deleted server-side
await api.ResolveTicket(new ResolveTicketCommand(id, note));
// after (verify existence first, tolerate not-found)
var ticket = await api.GetTicket(id);
if (ticket is null) { /* refresh list / show user */ return; }
await api.ResolveTicket(new ResolveTicketCommand(id, note)); Defensive patterns
Strategy: try-catch
Validate before calling
const exists = await api.getTicket(id);
if (!exists) throw new SkipError('Ticket ' + id + ' not found'); Try / catch
try {
await resolveTicket(id, note);
} catch (ApiError e) when (e.Status === 404) {
refreshTicketList();
notify('Ticket no longer exists');
} Prevention
- Always source ticket ids from a fresh list/get call, never from long-lived client state.
- Check the ticket's deleted flag in the UI and disable the resolve action.
- Ensure the tenant context (header/subdomain) is set before ticket calls.
When it happens
Trigger: Calling the ResolveTicket endpoint/command with a TicketId that does not exist in the Tickets table, or that exists but is soft-deleted (the query runs with default query filters, so rows flagged with the SoftDelete filter are invisible), or that belongs to a different tenant (tenant isolation filter on BaseDbContext excludes it).
Common situations: Client caches a ticket id from a previous session and the ticket was since deleted; a caller tries to resolve a ticket from another tenant; the id was mistyped or truncated; the ticket was soft-deleted by a concurrent user between listing and resolving; test fixtures were reset so old ids no longer exist.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/0b269355c2513a23.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/ResolveTicket/ResolveTicketCommandHandler.cs:19
using FSH.Framework.Core.Exceptions;
using FSH.Modules.Tickets.Contracts.v1.Tickets;
using FSH.Modules.Tickets.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Tickets.Features.v1.Tickets.ResolveTicket;
public sealed class ResolveTicketCommandHandler(TicketsDbContext dbContext)
: ICommandHandler<ResolveTicketCommand, Guid>
{
public async ValueTask<Guid> Handle(ResolveTicketCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var ticket = await dbContext.Tickets
.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Ticket {command.TicketId} not found.");
ticket.Resolve(command.ResolutionNote);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return ticket.Id;
}
}
View on GitHub (pinned to 3f2959e683)