Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/data/search-index.json

Large diffs are not rendered by default.

226 changes: 226 additions & 0 deletions docs-src/adr/130-per-engine-data-source-strategy.md

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions docs-src/adr/131-same-origin-api-proxy.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions docs-src/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Accepted ADRs explaining *why* the core cross-cutting patterns exist. Read these before changing a
pattern they describe: they capture context and trade-offs that aren't obvious from the code.

**129 accepted ADRs**, 001-129, numbered contiguously with one row per ADR in the table below.
**131 accepted ADRs**, 001-131, numbered contiguously with one row per ADR in the table below.
This file owns the ADR count and range (`MMCA.Common/FACTS.md`): read them here rather than
recounting, and when you add an ADR, update this line with its row. `tools/adr-index.ps1` in this
repo re-derives both from the files and fails if this line, the table, or a link drifts from them;
Expand Down Expand Up @@ -140,10 +140,13 @@ CI runs it on every push and pull request.
| [127](127-actor-model-not-adopted.md) | The actor model is not adopted | Weighed 2026-09-22 and rejected: no workload has high-cardinality per-entity in-memory state with single-writer semantics, since live polls, bookmarks and session state are served by EF behind the row-version concurrency token plus Redis output caching plus the SignalR hub, and the load is a single conference's attendance. Microsoft Orleans hosted under Aspire was the alternative weighed. Revisit when a real-time per-entity state workload appears that optimistic concurrency plus Redis cannot carry, and host Orleans per module behind the existing module contract. |
| [128](128-time-as-an-input.md) | Time as an input | Domain and Application code never reads the ambient clock; time arrives through an injected `TimeProvider`, with `BaseDomainEvent.DateOccurred` the single framework exemption. An IL-scanning fitness rule shipped as `ClockReadTestsBase` bans the clock getters; MMCA.Common, MMCA.ADC and MMCA.Store run it, MMCA.Helpdesk does not yet. |
| [129](129-tactical-aggregate-contract.md) | The tactical aggregate contract | One entity contract: sealed classes over `BaseEntity` / `AuditableBaseEntity` / `AuditableAggregateRootEntity`, construction only through a `Create` returning `Result<T>`, no public setters, child access through the root, invariants in static `*Invariants` classes composed with `Result.Combine`. `EntityConventionTestsBase` and the naming rules gate construction, sealing, setters, placement and static invariant classes; child access and `Result.Combine` stay convention. |
| [130](130-per-engine-data-source-strategy.md) | Per-engine data source strategy | One internal `IDataSourceEngine` per engine (SQL Server, PostgreSQL, SQLite, Cosmos DB), looked up through the static `DataSourceEngines.For(DataSource)` registry (static because conventions and SQL builders run without a container), so call sites read engine facts instead of branching on the kept `DataSource` enum. Capabilities collapse to `IsRelational` plus the few that differ (migrations, required connection string, null ordering, row version), and outbox support folds into `IsRelational`. `RequestIdentityInsert` is renamed `RequestExplicitKeyInsert`; the raw-SQL executor is registered only on a relational default engine; SQLite quotes with double quotes everywhere. Per-engine settings properties kept; a dictionary-keyed settings model rejected 2026-10-01. |
| [131](131-same-origin-api-proxy.md) | Same-origin API proxy (backend-for-frontend) | Opt-in per Blazor Web host (TD-08 Option A, MMCA.Common 1.218.0): `AddCommonSameOriginApiProxy` plus `MapCommonSameOriginApiProxy` in the server-only MMCA.Common.UI.Web serve `/api/**` on the UI host's own origin and forward to the gateway through YARP, attaching the bearer read from the existing HttpOnly session cookie, so the browser holds only an unsigned `alg:none` claims copy that authorizes nothing. Every proxied request, WebSocket upgrades included, must be same-origin by `Origin` and `Sec-Fetch-Site`; `OPTIONS` is answered locally and never forwarded; unsafe methods also need `X-CSRF: 1`. Stateless (no server-side session store); hubs and file downloads go through it too; a refused refresh clears the cookies and answers 401 while an undecidable one keeps them and answers 503 with `Retry-After`. Cookies default to `SameSite=Strict` under the proxy, `Lax` allowed (CSRF rests on the header and origin gates). MAUI is excluded. Rejected: a same-site cookie straight to the API (shared-domain mandate), stopping at the C+ hybrid, a session store, hosting in MMCA.Common.UI or MMCA.Common.API. Costs: one hop, the UI host becomes an auth edge, its rate limiter meters API traffic, and ingress hosts must call `UseCommonUiForwardedHeaders()` or every browser POST is refused. |

## Writing a new ADR

Copy the structure of an existing record: **Status** (Proposed / Accepted / Superseded, date and
Start from `docs-src/adr/_template.md` in this repo (it is not rendered; the generator only picks
up `NNN-*.md`), which carries the structure every record uses: **Status** (Proposed / Accepted / Superseded, date and
link when superseding), **Context** (the forces and the problem), **Decision** (what we chose, in
enough detail to implement), **Rationale** (why this over the alternatives), **Trade-offs** (what it
costs), and **Alternatives rejected** whenever something was genuinely considered and dropped.
Expand Down
35 changes: 35 additions & 0 deletions docs-src/adr/_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ADR-NNN: Title in Title Case (One-Line Qualifier When Useful)

<!--
Template for a new Architecture Decision Record. Copy this file to `NNN-kebab-title.md` (the next
free number), fill every section, delete the guidance comments, and add a row to README.md; the
`tools/adr-index.ps1` check fails until the row and the count line agree. This file is not rendered:
the generator only picks up `NNN-*.md`.
Cite source as `Repo/path/File.cs:line` so a reader and the ADR audit can open the evidence.
Plain ASCII; no em dashes.
-->

## Status
Proposed | Accepted | Superseded by [ADR-NNN](NNN-title.md) (YYYY-MM-DD).

## Context
<!-- The forces and the problem: what made a decision necessary now, with path:line evidence. Name
the ADRs this one builds on or constrains. -->

## Decision
<!-- What we chose, in enough detail to implement and to check. Name the types, the extension point
and the gate (fitness test, analyzer, CI step) that keeps the decision true, if any. -->

## Rationale
<!-- Why this over the alternatives, in terms of the forces listed in Context. -->

## Trade-offs
<!-- What it costs: complexity, performance, flexibility given up, consumer impact. -->

## Alternatives rejected
<!-- Each genuinely considered option: what it was, the date it was weighed, why it lost, and the
condition that would make it right to revisit. An unrecorded rejection gets re-proposed. Delete
this section only if nothing was considered. -->

## Consequences
<!-- Follow-on work, consumer adoption steps, and what to watch. Optional. -->
Loading
Loading