Validation, Error UX & Safe Save Pipelines
The Work Order editor gets layered validation rules as plain C#
(WorkOrderValidator, WorkOrderRules, an explicit status-transition table), a reusable SaveWorkOrderCommand
that carries the intent through the pipeline, field-level errors painted by an ErrorProvider plus a summary
panel that lists every problem, and a safe save pipeline in WorkOrderService.SaveAsync —
validate → rules & authorize → persist atomically → confirm — where nothing is written unless every step
passes and an unexpected failure reaches the user as one safe sentence.
Nothing here is deployed anywhere; it is a plain Wisej.NET 4 project that runs on your machine.
Run it
cd "LearnWisej-Samples/Production Architecture Deep Dive\Module 5\TicketOps"
dotnet run -f net10.0 --urls http://localhost:5105
Then open http://localhost:5105. (Visual Studio: open TicketOps.slnx, press F5 — the port is in
Properties/launchSettings.json.)
Requirements: .NET 10 SDK, the Wisej-4 4.1.0 NuGet package.
What to try in the Work Orders window
#2002 is selected on load. Acting as sets the session's role (the server reads it; the command never carries it).
| Action | Path | What you should see |
|---|---|---|
| Change the title, Save | success | status ● Work order #2002 saved.; the grid reloads |
| Clear the title and set cost 25000, Save | validation | a red glyph beside each field (hover for the message); summary panel "2 problems need attention"; status ● 2 problems found — nothing was saved |
| Set the due date in the past, Save | due-date rule | glyph on Due date: "Due date can't be in the past." |
| Set 1,200 estimated hours, Save | range | glyph on Estimated hours: "Hours must be between 0 and 999." |
| Select closed #2006, change the title, Save | business rule | no glyph (every field is valid); the panel reads "Closed work orders cannot be edited. Ask a Supervisor to reopen it." |
| As Technician, set #2002's cost to 9,500, Save | role restriction | the panel reads "Only a Supervisor may set a cost above $2,500."; switch Acting as to Supervisor and it saves |
| New / ↻ Refresh | — | clears the editor / reloads and discards unsaved values |
If the commit fails, the transaction rolls back, the details go to the log, and the user sees only the red banner + toast The work order could not be saved. Your changes are still here — try again or contact support.; the editor keeps the edits.
Where things live
TicketOps/
├─ Views/
│ ├─ WorkOrderEditor.cs the screen: buttonSave_Click (collect → pre-check → service → show), ShowValidation, ReportFailure
│ └─ WorkOrderEditor.Designer.cs GENERATED-style layout incl. the ErrorProvider and the summary panel — no logic here
├─ Controls/StatusBanner reusable "● state" + banner UserControl (display only)
├─ Validation/
│ ├─ SaveWorkOrderCommand.cs the command: what the editor collected, immutable, no role
│ ├─ ValidationResult.cs ValidationError + the two channels (field / summary); errors are collected, not thrown
│ ├─ WorkOrderValidator.cs the pure validator (command in, result out) — runs in the editor, the service and the tests
│ └─ ValidationTestCases.cs the documented cases + ValidationTestRunner (no Wisej.NET type)
├─ Services/
│ ├─ IWorkOrderService.cs the contract the screen calls
│ └─ WorkOrderService.cs THE PIPELINE: validate → rules & authorize → persist (one tx) → confirm
├─ Domain/
│ ├─ WorkOrder.cs record + RowVersion (moves only on commit)
│ ├─ WorkOrderTransitions.cs the status machine as a table (Legal / Privileged / Illegal)
│ ├─ WorkOrderRules.cs rules that need the stored record or the role; UserRole
│ └─ SaveResult.cs Saved / Invalid + the collected errors
├─ Data/
│ ├─ IWorkOrderRepository.cs persistence + IWorkOrderTransaction (Upsert, Audit, CommitAsync, Dispose = rollback)
│ └─ InMemoryWorkOrderRepository.cs fake store, seeded
├─ Infrastructure/
│ ├─ ILog.cs / ActivityLog.cs logging (written to the server console; details stay there)
│ ├─ SessionContext.cs who is acting (role) — server-side, per session
│ └─ AppComposition.cs who gets what: one object graph per session, constructor injection, no statics
├─ Resources/Strings.cs safe user-facing messages (SaveFailed …)
├─ docs/ ValidationRules, SavePipeline (+ .svg), ErrorUxGuidelines, TestCases, ProductionReadinessNote
├─ Program.cs Wisej.NET session entry point → AppComposition
└─ Startup.cs Kestrel host (app.UseWisej())