Convention — lookup CRUD template
What it is
The repeatable recipe for adding the next lookup-admin CRUD feature. ManpowerIQ matured this template across seven reference-data entities — ShiftTemplate, Holiday, DemandReason, NodeType, Terminal, Grade, Skill — over sprints MIQ-131, MIQ-132 and MIQ-133. Following it means a new lookup gets a consistent 5-verb API, soft-delete, FK-protection, bilingual i18n, RBAC permissions, and a matching React admin screen, with no surprises.
When to use it
When you need full admin management (list / view / create / edit / delete) for a reference-data entity. If the entity already has a production/runtime surface, this admin track coexists alongside it — see Coexistence pattern.
The recipe
1. Pre-flight audit (do this first). Before writing code, confirm the shape (MIQ-131/132/133 reports, "pre-flight" sections):
- Entity base class —
TenantEntity(BU-scoped) vsAuditableEntity(global).AuditableEntitydescendants need an explicitHasQueryFilter(x => !x.IsDeleted)in the DbContext config or soft-deleted rows leak into lists;TenantEntitydescendants inherit the central filter (MIQ-132 decision 46). - Every FK pointing at this entity — each one is a separate "referenced" count surface for delete-blocking (see step 3).
- Arabic name nullability — it is per-entity, not global (4 of 7 entities require
Name_2_Arabic; DemandReason leaves it optional). Audit it independently; never blanket-copy the previous entity (MIQ-131 decision 40; MIQ-132 decision 47). - The
codeunique index must carryWHERE is_deleted = falseso a code can be reused after a soft-delete (PB-043 compliance).
2. Backend. Mirror the shape of an existing entity (Grade is the canonical 1-FK example):
Application/<Entity>s/—I<Entity>Service(the 5 verbs),<Entity>Dtos(ListItem / Detail / CreateRequest / UpdateRequest — noCodefield on UpdateRequest, code is immutable), event-type constants, and a<Entity>ReferencedExceptionif delete can be FK-blocked.Infrastructure/Services/<Entity>s/<Entity>Service— implements the interface; injectsIAuditLogger,ICurrentTenantProvider, andIStringLocalizer<ErrorMessages>(the service composes localized message bodies — the controller stays mechanical). Delete callsSoftDeleteExtensions.MarkDeleted(...).API/Controllers/<Entity>sController— route/api/admin/<entity-plural>, 5 verbs each[Authorize(Policy = "<entity>.view")]/".config", using the shared helpers (see Helpers catalog).API/Validation/<Entity>Validators— FluentValidation Create/Update validators, auto-registered.- Register
services.AddScoped<I<Entity>Service, <Entity>Service>()inProgram.cs.
3. Delete-protection. Two guards, in this order: reject if the row IsSystem (a seeded system row), then count each FK surface and, if any is non-zero, throw the <Entity>ReferencedException (409). See Exceptions.
4. i18n keys. Add the entity's keys to both ErrorMessages.resx and ErrorMessages.ar.resx in lockstep, following the BE i18n key convention: <Entity>.IsSystem.Body, <Entity>.Referenced.{Header, Intro, Surface.<Table>, Footer}, <Entity>.Validation.<Rule>.
5. Migration. Seed the two permissions (<entity>.view, <entity>.config) with category = "Reference Data"; add the partial-unique index migration if the code index lacks the is_deleted = false filter.
6. Frontend. api/<entity-plural>.ts (5 wrappers) → a hook file consuming the createLookupHooks factory → an <Entity>Table + dialog → a page using the shared DeleteConfirmDialog → route + SideNav entry under Reference Data → en.json/ar.json keys in parity. See Helpers catalog for the factory and dialog.
7. Tests. A render/permission test (web) asserting en/ar key parity; an integration test using the FK-block try/finally ownership pattern (see Test patterns).
Gotchas / constraints
codeis immutable on update — enforced by omitting it fromUpdateRequest; the FE disables the input as a visual backstop only.- Service composes the localized body, not the controller —
IStringLocalizer<ErrorMessages>is injected at the service ctor; the controller just maps the caught exception to a 409 with the code. - Don't
ORthe FK pre-checks together — count each surface separately so the "referenced by" message can name exactly which tables block the delete. - Arabic nullability per entity — re-audit each time; copying the previous entity's validator is the classic trap.
- Factory/dialog shapes are locked — if a new entity doesn't fit the shared factory or dialog, keep that entity's hooks/dialog inline rather than bending the abstraction. This is the §G discipline.
Build status
Available — the template is live across all seven lookup entities (MIQ-131/132/133). It is a convention, not a framework: there's no generator, you mirror an existing entity.
Related
- Exceptions, Helpers catalog, BE i18n keys, Description fields (D9)
- Coexistence pattern, §G discipline, Test patterns
- Source: MIQ-131 / MIQ-132 / MIQ-133 reports (
manpoweriq/docs/); fact sheet 21 (admin reference data).