fullstackhero/dotnet-starter-kit · warning · CustomException
Cannot a ticket in status — reopen it first.
Error message
Cannot {action} a ticket in status {Status} — reopen it first. What it means
The Ticket aggregate guards closed/resolved tickets: ThrowIfClosedOrResolved fires a 409 CustomException whenever a mutating action (e.g. Assign) is attempted on a ticket whose Status is Closed or Resolved. Tickets are meant to be immutable once resolved/closed until explicitly Reopen()ed. This is a domain-rule conflict, not a data error.
Solutions
- Check the ticket status before calling Assign (fetch it or expose Status on the DTO) and skip/warn if Closed or Resolved.
- Call ReopenTicket (ticket.Reopen()) first when the workflow genuinely requires reassigning a resolved ticket, then Assign.
- Catch CustomException with 409 Conflict in the endpoint layer and surface 'reopen the ticket first' to the user.
- Have the UI disable the Assign action when the ticket's status is Resolved/Closed to prevent the doomed call.
Example fix
// before
await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user });
// after
var ticket = await dbContext.Tickets.FindAsync([id]);
if (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })
await mediator.Send(new ReopenTicketCommand { TicketId = id });
await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user }); Defensive patterns
Strategy: validation
Validate before calling
var ticket = await dbContext.Tickets.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id);
if (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })
throw new InvalidOperationException("Reopen the ticket before assigning."); Type guard
static bool CanAssign(Ticket t) => t is { Status: not TicketStatus.Closed and not TicketStatus.Resolved }; Try / catch
try { await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user }); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { NotifyUser("Ticket must be reopened first."); } Prevention
- Read ticket Status before any mutating call and branch in the UI.
- Disable Assign/Close actions client-side when status is Resolved or Closed.
- Catch 409 CustomException centrally in the API client and map it to a friendly message.
- Prefer explicit Reopen then Assign in automation workflows instead of assuming assign-on-resolved works.
When it happens
Trigger: Calling ticket.Assign(assigneeUserId) (via AssignTicketCommand) while Status == Closed or Resolved; any future action routed through ThrowIfClosedOrResolved with action names like 'assign' on a closed/resolved ticket.
Common situations: Stale UI showing a resolved ticket where an operator clicks 'Assign'; two agents working the same ticket — one closes it, the other submits an assignment that lands after; automated workflows assigning resolved tickets for follow-up.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- A closed ticket cannot be resolved — reopen it first.
- Only a resolved ticket can be closed — current status is
- A closed ticket cannot accept new comments — reopen it…
- A closed ticket cannot be edited — reopen it first.
- Top-up request cannot be rejected because it is (only…
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/eecb2ec81812d83d.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs:234
private void TransitionStatus(TicketStatus next)
{
if (next == Status)
{
return;
}
var previous = Status;
Status = next;
AddDomainEvent(DomainEvent.Create<TicketStatusChangedDomainEvent>(
(id, ts) => new TicketStatusChangedDomainEvent(Id, previous, next, id, ts)));
}
private void ThrowIfClosedOrResolved(string action)
{
if (Status is TicketStatus.Closed or TicketStatus.Resolved)
{
throw new CustomException(
$"Cannot {action} a ticket in status {Status} — reopen it first.",
(IEnumerable<string>?)null,
HttpStatusCode.Conflict);
}
}
}
View on GitHub (pinned to 3f2959e683)