Simultaneous close of a Standard motion throws an uncaught DbUpdateConcurrencyException (BifrostDbContext.cs:288) #30

Closed
opened 2026-11-09 22:07:44 +00:00 by heimdall · 0 comments
Member

#21 added entity.Property(x => x.Status).IsConcurrencyToken() to the Motion entity (BifrostDbContext.cs:288) for the sealed-ballot close. It applies to every motion, Standard ones included.

MotionGovernanceService.CloseMotionAsync (~:119-128) sets Status and calls SaveChangesAsync with no catch. Two members closing the same Standard motion at once (or a double-submit of the Close form) now run UPDATE ... WHERE Id = @id AND Status = <value read> twice; the second matches no row and EF throws DbUpdateConcurrencyException. The caller, Pages/App/Motions/Index.cshtml.cs OnPostCloseAsync, does not catch it, so the request falls to UseExceptionHandler("/Error") (Program.cs:270): the loser gets the error page (HTTP 500), not the redirect. Before #21 the second close silently overwrote the first.

Expected: the loser reloads, sees the motion already closed by the winner, records no second motion.closed audit event, and redirects as normal. The sealed closer already does this (SealedBallotService.cs:307).

Not verified: the SQL EF emits, behaviour on Postgres (the finding is from reading the model and the in-memory provider). Fix and test in the linked PR.

#21 added `entity.Property(x => x.Status).IsConcurrencyToken()` to the `Motion` entity (`BifrostDbContext.cs:288`) for the sealed-ballot close. It applies to every motion, Standard ones included. `MotionGovernanceService.CloseMotionAsync` (~:119-128) sets `Status` and calls `SaveChangesAsync` with no catch. Two members closing the same Standard motion at once (or a double-submit of the Close form) now run `UPDATE ... WHERE Id = @id AND Status = <value read>` twice; the second matches no row and EF throws `DbUpdateConcurrencyException`. The caller, `Pages/App/Motions/Index.cshtml.cs` `OnPostCloseAsync`, does not catch it, so the request falls to `UseExceptionHandler("/Error")` (`Program.cs:270`): the loser gets the error page (HTTP 500), not the redirect. Before #21 the second close silently overwrote the first. Expected: the loser reloads, sees the motion already closed by the winner, records no second `motion.closed` audit event, and redirects as normal. The sealed closer already does this (`SealedBallotService.cs:307`). Not verified: the SQL EF emits, behaviour on Postgres (the finding is from reading the model and the in-memory provider). Fix and test in the linked PR.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost#30
No description provided.