Skip to content

Convention — helpers catalog

What it is

The small set of shared helper/extension classes extracted during the lookup-CRUD work (MIQ-131/132/133) that a developer building the next feature should reuse rather than re-implement. Each was extracted from observed duplication, not predicted need — see §G discipline — so the catalog is deliberately short.

When to use it

Reach for these when building a lookup-CRUD feature (or any controller that returns ProblemDetails / any service that soft-deletes). Don't extend their locked shapes; if your case doesn't fit, keep your code inline.

Backend helpers

Helper File What it does
SoftDeleteExtensions Application/Common/SoftDeleteExtensions.cs MarkDeleted(this AuditableEntity, string actor, DateTimeOffset now) — sets IsDeleted=true, DeletedAt, DeletedBy, IsActive=false in one call. Used by every lookup Delete path.
ControllerProblemExtensions API/Common/ControllerProblemExtensions.cs RFC 7807 ProblemDetails factory: Problem400(detail), ToValidationProblem(validationResult), and ProblemWithCode(status, title, detail, extensions) for the structured-code 409s.
ExceptionPredicates API/Common/ExceptionPredicates.cs IsNotFound() / IsDuplicate() predicates on InvalidOperationException, for catch (… ex) when (ex.IsDuplicate()) mapping to the right status.
SystemRowProtectedException<T> Application/Common/SystemRowProtectedException.cs The generic system-row guard (MIQ-133 Phase 1, the PB-085 rule-of-three extraction): (T entity, string code, string localizedMessage), carrying Entity and Code. See Exceptions.

Frontend helpers

Helper File What it does
createLookupHooks web/src/lib/createLookupHooks.ts React Query factory: given { resourceKey, api: { list, get, create, update, delete } }, returns { keys, useList, useDetail, useCreate, useUpdate, useDelete }. All seven lookup hook files consume it.
DeleteConfirmDialog web/src/components/common/DeleteConfirmDialog.tsx Shared delete-confirmation modal. Takes the delete-mutation hook reference + rowId + testIdPrefix; renders server 409 bodies with whitespace-pre-line so multi-line "referenced by" messages keep their line breaks.
extractErrorMessage web/src/lib/apiError.ts Safely pulls the user-facing message out of an Axios error (response.data.detail ?? message ?? null). Used by every dialog.

How they fit together

A typical lookup delete: the page renders DeleteConfirmDialog, passing its useDelete<Entity>() mutation (from a createLookupHooks factory) and a testIdPrefix. On confirm, the mutation calls the API; a 409 comes back from ProblemWithCode on the server; extractErrorMessage surfaces the localized detail; the dialog shows it with line breaks preserved.

Gotchas / constraints

  • The factory and dialog shapes are locked (MIQ-131 decision 39). If a future entity needs a 7th parameter or a different verb, do not extend the helper — keep that entity's hooks/dialog inline, or split the abstraction. Forcing a parameter onto the shared shape is the leaky-abstraction failure the §G discipline exists to prevent.
  • useSkillsAdmin uses resourceKey: 'skills-admin' to partition its React Query cache away from the production skills surface — an example of the coexistence pattern.
  • SystemRowProtectedException<T> reached the catalog at three real consumers, not before — the rule-of-three threshold (MIQ-133, PB-085). Some 4-consumer patterns (the ComposeReferencedBody StringBuilder) were deliberately not extracted because inline still read cleaner.

Build status

Available — all helpers are live and in use across the lookup-CRUD entities (MIQ-131/132/133).