# Engineering Conventions

> **TLDR**
>
> - .NET 10+ idioms (primary constructors, collection expressions); XML docs on public members; no magic strings; no deprecated .NET Framework libraries.
> - Every domain class: parameterless constructor **plus** an `Object? object` copy constructor (`protected` on non-sealed, `private` on sealed).
> - **No inline `style=`** — every visual value lives in `alpa-components.css`; each UI subsystem owns its class prefix; never reuse a class name across subsystems.
> - Interactive controls must have real state: a C# backing field + `@onclick`. A static `--active` class with no handler is not acceptable.
> - Destructive-reading actions (delete, remove, clear) need an arm/confirm double-tap matched to their presentation — a red color alone is not a confirmation, even if the action is actually recoverable.
> - Raw/runtime data → domain objects goes through `RawRepresentationFactory<T>`; configuration goes through the Options pattern. Never mix them.
> - Content → component ViewModels go through dedicated adapter factories: policies are structural (not documentary), canonical-first/legacy-fallback field mapping, wire discriminators pinned by tests, degrade-per-unknown-item/strict-per-call, WebView-unusable values never reach the ViewModel.
> - Working against a specific Microsoft API? Search `site:learn.microsoft.com` first; default to latest standards.

## Code style

- .NET 10+ idioms (primary constructors, collection expressions) unless the project version dictates otherwise.
- XML doc comments on public members.
- Logical grouping of related members with descriptive comments; clean separation of concerns; consistent naming per .NET conventions.
- No magic strings — use constants or strongly-typed configurations.
- Do not use deprecated .NET Framework libraries.

## Domain model constructors

Every domain class needs a parameterless constructor **and** an `Object? object` copy
constructor — `protected` for non-sealed/base classes, `private` for `sealed` classes
(`protected` on a sealed type is meaningless and triggers CS0628).

Verified against LiteDB 5.0.21: the extra constructor does not affect (de)serialization —
LiteDB's reflection-based instantiation targets the public parameterless constructor
specifically and ignores other overloads.

## Blazor / CSS class rules

- **No inline `style=` attributes.** Every visual value belongs in `alpa-components.css`. If the same override appears more than once, it must be a named CSS class. (Data-bound dynamic values — e.g. a CSS custom property carrying a percentage — are the accepted exception.)
- **Each UI subsystem owns its class prefix** (e.g. `alpa-notif-card-*`, `alpa-notif-row-*`). Never reuse a class name across two subsystems — a duplicate selector later in the file silently overrides the earlier one.
- **Shared utilities go in the `Utility classes` block** at the end of `alpa-components.css`, not scattered inline.
- **The same rule applies to markup, not just CSS: a second surface needing the same visual means a component, not a second copy.** Pull the markup into a component and have both callers render it. **Which folder it goes in is its own decision** — `Components/Library/` is the feed-hydration set, feature folders like `Components/FTDT/` and `Components/Notifications/` are for statically implemented areas. The test, and what going wrong looks like, are in [N-Tier Architecture § Two kinds of component](N-TIER-ARCHITECTURE.md#two-kinds-of-component). This was unwritten until AB#2286 and it is where drift starts — the CSS rule above stops duplicate *styles*, but nothing stopped duplicate *cards*, so the comms notification row lived inline in one page and was about to be re-typed into a second. A near-identical copy is worse than an obvious one: it looks right, and it silently stops tracking the original. Give the component the visual only — let the host own behaviour via `EventCallback` — so the same card can sit on a live list and on a seeded test surface without either special-casing the other.
- **Interactive controls must have state.** Any control that visually changes on tap (pill, tab, toggle) requires a C# backing field and `@onclick` in the Razor file.
- **Destructive-reading actions get a confirm step matched to their real risk, not just a red color.** A button styled/labeled as destructive (delete, remove, clear) must arm on first tap and require a second tap to actually execute — mirror `FTDTEditDutyPeriodViewModel`'s `Concluding`/`BeginConclude`/`CancelConclude` state machine (`FTDTDutyPeriodShell.razor` + `FTDTEditDutyPeriod.razor`, AB#2288 adds the matching `Deleting`/`BeginDelete`/`CancelDelete` for Delete). This applies even when the action is actually safe/recoverable (e.g. FTDT's DELETE archives rather than hard-deletes) — the button's presentation still reads as permanent, and the confirm step must match what the user believes is about to happen, not just what the code actually does. A distinct color alone is not a confirmation.
- **One tap target per component — never nested anchors.** A component whose ViewModel `Link` is set renders as (or inside) an `<a>`; a composed child that renders its own `<a>` (e.g. a CTA `Button`) is mutually exclusive with that. The mapper enforces it: when it composes a link-bearing child, it nulls the parent-level `Link`. Nested anchors are invalid HTML and browsers split them unpredictably.

## RawRepresentationFactory vs the Options pattern

Never instantiate domain objects directly from raw DTOs or JSON strings in service logic —
use a dedicated `RawRepresentationFactory<T>`:

```csharp
var domainObj = _factory.CreateFromRaw(rawPayload);
```

**The factory is for runtime data; Options is for configuration.** For startup-bound
settings, feature flags, fallback strings and formatting rules use `IOptions<T>`,
`IOptionsMonitor<T>` (live reload) or `IOptionsSnapshot<T>` (scoped), registered via
`AddOptions<T>().Bind(...).ValidateDataAnnotations().ValidateOnStart()` or an
`AddXxx(options => { ... })` extension. The two are orthogonal:

- Never carry per-request data in `IOptions<T>`; never put settings in the factory.
- Keep the factory a **pure mapper** — it must not hold an `IServiceProvider` and resolve dependencies at runtime (service-locator anti-pattern).
- If a flow needs both, inject `IOptions<T>` into the service that gathers data and let the factory only map.

### Content→component adapter rules

The Blazor-side application of the factory rule above: content/domain models map onto
component-library ViewModels through dedicated adapter factories, never inline
field-by-field assignment in a page or ViewModel. Current roster, what each maps, and
consumers: see
[N-TIER-ARCHITECTURE.md § Content-to-Component Mapping (WI-2262)](N-TIER-ARCHITECTURE.md#content-to-component-mapping-wi-2262)
— not duplicated here, so this list doesn't go stale as factories are added. Rules proven
out building and reconciling them:

- **Shape:** sealed class, stateless, DI singleton, constructor-injected into consumers.
  Async lookups stay with the caller — the factory only maps what is already resolved.
- **Policies are structural, not documentary.** If a factory owns a value policy (e.g.
  `DocumentHeroFactory.OpenRoute` owns the `/document-open?fileId=` route), its `Create`
  takes the raw key and builds the value internally. Never accept a pre-built policy string
  a caller could construct wrong — an XML doc saying "build it with X" is not enforcement.
- **Canonical-first, legacy-fallback.** Map contract-canonical DTO fields first and
  prototype-era fields as the `??` fallback (`w.Description ?? w.Blurb`, `w.Image ?? w.Icon`).
- **Wire discriminators are API contract.** `ItemType`/`ContainerType` strings in factory
  arms are quoted literals pinned by unit tests (`ItemComponentFactoryTests.cs`,
  `ContainerFactoryTests.cs`, `ContentComponentFactoryTests.cs`); a test breaking on one is
  a backend-coordination event, not a refactor.
- **Degrade per unknown item, never per valid item — strict per direct call.** Collection
  mapping skips items of an unrecognized type so one new backend type never kills a whole
  page render (`ItemComponentFactory.CreateAll` catches `NotSupportedException` per item;
  `ContainerFactory.TryCreate` returns `null` for the same reason — same principle, two
  mechanisms, pick whichever fits the factory's call shape). A direct single-item `Create`
  on a genuinely unrecognized type throws instead of silently returning nothing, so tests
  and direct callers surface contract drift loudly.
- **Values the WebView can't use never reach the ViewModel.** Native-only resource names and
  Shell page names are gated at the mapper (rooted `/…` or absolute `http…` only — see
  `MenuItemCardFactory.IsWebNavigable`) — a raw native path in an `href` is a P1 (dead
  WebView navigation).

## Research first

For tasks involving a specific Microsoft API, perform a targeted search on
`site:learn.microsoft.com` before writing code. Default to the latest standards unless the
project version dictates otherwise.
