{"record":{"id":"0b269355c2513a23","repo":"fullstackhero/dotnet-starter-kit","slug":"ticket-command-ticketid-not-found-0b2693","errorCode":null,"errorMessage":"Ticket {command.TicketId} not found.","messagePattern":"Ticket (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/ResolveTicket/ResolveTicketCommandHandler.cs","lineNumber":19,"sourceCode":"using FSH.Framework.Core.Exceptions;\nusing FSH.Modules.Tickets.Contracts.v1.Tickets;\nusing FSH.Modules.Tickets.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Tickets.Features.v1.Tickets.ResolveTicket;\n\npublic sealed class ResolveTicketCommandHandler(TicketsDbContext dbContext)\n    : ICommandHandler<ResolveTicketCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(ResolveTicketCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var ticket = await dbContext.Tickets\n            .FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Ticket {command.TicketId} not found.\");\n\n        ticket.Resolve(command.ResolutionNote);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return ticket.Id;\n    }\n}\n","sourceCodeStart":1,"sourceCodeEnd":26,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/ResolveTicket/ResolveTicketCommandHandler.cs#L1-L26","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before (id from stale client cache)\nvar id = ticketList[0].Id; // ticket already deleted server-side\nawait api.ResolveTicket(new ResolveTicketCommand(id, note));\n// after (verify existence first, tolerate not-found)\nvar ticket = await api.GetTicket(id);\nif (ticket is null) { /* refresh list / show user */ return; }\nawait api.ResolveTicket(new ResolveTicketCommand(id, note));","handlingStrategy":"try-catch","validationCode":"const exists = await api.getTicket(id);\nif (!exists) throw new SkipError('Ticket ' + id + ' not found');","typeGuard":null,"tryCatchPattern":"try {\n  await resolveTicket(id, note);\n} catch (ApiError e) when (e.Status === 404) {\n  refreshTicketList();\n  notify('Ticket no longer exists');\n}","preventionTips":["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."],"tags":["ef-core","not-found","cqrs","tickets","soft-delete"],"backgroundTag":"entity-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"}