Skip to content

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>ReferencedException carries Id, a Code constant ("<ENTITY>_REFERENCED"), and a Referencers dictionary 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 instance Code (e.g. "GRADE_PROTECTED"), and a pre-localized message. The wire codes were unified to <ENTITY>_PROTECTED across all entities in MIQ-133 Phase 1 (the historical SYSTEM_REASON_PROTECTED was retrofitted to DEMAND_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

  1. System-row guard first — if IsSystem, throw SystemRowProtectedException<T> before touching any FK surface. System rows are immutable regardless of references.
  2. FK surfaces next — count each referencing table; if any is non-zero, throw <Entity>ReferencedException with the composed body.
  3. Otherwise soft-delete.

Gotchas / constraints

  • Compose multi-line bodies in the service — never in the controller or the exception constructor. The IStringLocalizer lives 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, not code — no frontend consumer gates on the code string, so wire-format renames are safe; the code is 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.
  • Code is a constant on the Referenced exceptions, an instance property on SystemRowProtectedException<T> — read ex.Code for the latter.

Build status

Available — live across the seven lookup entities and the allocation-rule services (MIQ-131/132/133).