Convention — exceptions & the _PROTECTED / _REFERENCED wire format
What it is
How ManpowerIQ turns a domain-rule violation (you can't delete this — it's referenced; you can't edit this — it's a system row) into a consistent HTTP response: a 409 Conflict RFC 7807 ProblemDetails carrying a structured code in its extensions, with a localized human-readable body. Established for the lookup-CRUD entities across MIQ-131/132/133.
When to use it
Whenever a service rejects a write for a domain reason a client should be able to handle distinctly: a delete blocked by foreign keys, or a mutation blocked because the target is a seeded system row.
The pattern
There is no DomainException base class in this codebase — domain exceptions inherit directly from InvalidOperationException (MIQ-133 Phase 1 audit). Two named exception shapes carry a structured code:
| Trigger | HTTP | code |
Exception |
|---|---|---|---|
| Delete a row still referenced by FKs | 409 | <ENTITY>_REFERENCED |
<Entity>ReferencedException : InvalidOperationException |
Update/delete a seeded system row (IsSystem = true) |
409 | <ENTITY>_PROTECTED |
SystemRowProtectedException<T> : InvalidOperationException |
| Create with a duplicate code | 409 | — | plain InvalidOperationException ("already exists"), caught by an IsDuplicate() predicate |
| Validation failure | 400 | — | FluentValidation, mapped via ToValidationProblem() |
<Entity>ReferencedExceptioncarriesId, aCodeconstant ("<ENTITY>_REFERENCED"), and aReferencersdictionary of the non-zero FK counts. Per-entity subclasses exist:GradeReferencedException(1 FK),ShiftTemplateReferencedException(3),SkillReferencedException(4),TerminalReferencedException(8), etc. (Application/<Entity>s/<Entity>ReferencedException.cs).SystemRowProtectedException<T>is the generic system-row guard extracted in MIQ-133 (Application/Common/SystemRowProtectedException.cs); it carries the entity, an instanceCode(e.g."GRADE_PROTECTED"), and a pre-localized message. The wire codes were unified to<ENTITY>_PROTECTEDacross all entities in MIQ-133 Phase 1 (the historicalSYSTEM_REASON_PROTECTEDwas retrofitted toDEMAND_REASON_PROTECTED).
The response. The controller catches the exception and calls the shared ProblemWithCode(...) helper (see Helpers catalog):
catch (SystemRowProtectedException<Grade> ex)
{
return this.ProblemWithCode(
StatusCodes.Status409Conflict,
"System grade cannot be modified",
ex.Message, // pre-localized by the service
new Dictionary<string, object?> { ["code"] = ex.Code }); // "GRADE_PROTECTED"
}
yielding:
{
"status": 409,
"title": "System grade cannot be modified",
"detail": "This grade is a system row and cannot be modified or deleted.",
"extensions": { "code": "GRADE_PROTECTED" }
}
The body is composed in the service, not the controller. The service injects IStringLocalizer<ErrorMessages> and builds the multi-line "referenced by" body from the resx key hierarchy (<Entity>.Referenced.{Header, Intro, Surface.<Table>, Footer}), emitting only the FK surfaces whose count is non-zero. The controller stays mechanical. See BE i18n keys.
Order of checks on delete
- System-row guard first — if
IsSystem, throwSystemRowProtectedException<T>before touching any FK surface. System rows are immutable regardless of references. - FK surfaces next — count each referencing table; if any is non-zero, throw
<Entity>ReferencedExceptionwith the composed body. - Otherwise soft-delete.
Gotchas / constraints
- Compose multi-line bodies in the service — never in the controller or the exception constructor. The
IStringLocalizerlives in the service. - Only emit non-zero FK lines — the "referenced by" message names exactly the tables that block the delete; zero-count surfaces are omitted.
- The FE reads
detail, notcode— no frontend consumer gates on thecodestring, so wire-format renames are safe; thecodeis for structured/programmatic handling. - A new entity must add its resx keys or the service throws a missing-resource error at runtime when composing the body.
Codeis a constant on the Referenced exceptions, an instance property onSystemRowProtectedException<T>— readex.Codefor the latter.
Build status
Available — live across the seven lookup entities and the allocation-rule services (MIQ-131/132/133).
Related
- Helpers catalog —
ProblemWithCode,ExceptionPredicates,SystemRowProtectedException<T>. - BE i18n keys — the
<Entity>.Referenced.*/.IsSystem.Bodykey scheme. - Lookup CRUD template, Coexistence pattern.
- Source: MIQ-131 / MIQ-132 / MIQ-133 reports (
manpoweriq/docs/).