mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 23:33:10 +00:00
The org PR template pre-filled Summary / Validation / Tests / Notes here while REVIEW_AND_PR_FRAMEWORK.md section 8 prescribes Summary / Changes and value / Ticket. A repo template now overrides the org one, and the framework notes the divergence from the org pr-policy workflow, which is not wired in. README.md orients a reader: environments, architecture in one line, project map, local commands, the governance gate, deployment, and a documentation map. Cleanup: TODO.md is removed because Jira owns work status and its items are stale or done. BACKEND_ARCHITECTURE.md is removed as superseded; the two references now point at git history. .env.example loses its BOM and mojibake dashes. .gitattributes keeps its one active rule.
272 lines
14 KiB
Markdown
272 lines
14 KiB
Markdown
# Architecture and Code Quality (canonical)
|
|
|
|
> **Status: canonical.** This document owns the *meaning* of the backend's
|
|
> architecture and code-quality rules. It replaced the earlier
|
|
> `BACKEND_ARCHITECTURE.md`, which now lives only in git history.
|
|
>
|
|
> Companion documents:
|
|
> - `QUALITY_GATES.md` — *how* each rule is enforced (commands + CI mapping).
|
|
> - `REVIEW_AND_PR_FRAMEWORK.md` — *who* reviews, in what order, with what
|
|
> evidence, and how exceptions are recorded.
|
|
> - `AGENTS.md` — precedence and repo-specific agent delta.
|
|
|
|
## 1. Layering model
|
|
|
|
The backend is a feature-oriented service boundary over Entity Framework Core:
|
|
|
|
```text
|
|
HTTP controller -> I{Feature}Service -> I{Feature}DataService -> ApplicationDbContext
|
|
```
|
|
|
|
The interfaces are **architectural seams**, not a mandate to wrap every class or
|
|
every EF method. Add an interface when it separates HTTP, business,
|
|
persistence, or external I/O responsibility *and* enables behavior-focused
|
|
testing through the public contract.
|
|
|
|
| Layer | May depend on | May not depend on |
|
|
|-------|---------------|-------------------|
|
|
| API controller | `I{Feature}Service`, `ILogger`, framework types | `DbContext`, concrete services, concrete data services, raw SQL, business workflow code |
|
|
| Business service (`SeaHaven.Services`) | `I{Feature}DataService`, `IOptions<T>`, `ILogger`, pure ports (`IServicePorts`, `IDispatchEmailPort`, …) | `DbContext`, `IConfiguration`, concrete data services, a generic repository |
|
|
| Data service (`SeaHaven.DataServices`) | `ApplicationDbContext`, EF Core | Business decisions, HTTP concerns |
|
|
| Composition / infrastructure | `Program.cs`, `*Module.cs`, migrations, Identity | Being depended on by controllers or business services |
|
|
|
|
### 1.1 Controllers
|
|
|
|
- Parse HTTP input, authorize the caller, invoke a business-service interface,
|
|
and map the result to the established HTTP contract.
|
|
- Depend **only on `I{Feature}Service` interfaces** — never on `DbContext`, EF,
|
|
concrete service implementations, or concrete data-service implementations.
|
|
- Contain no persistence queries and no business workflows.
|
|
- Map known failures explicitly; let the centralized exception boundary handle
|
|
unexpected failures (see §5).
|
|
|
|
### 1.2 Business services
|
|
|
|
- Own validation, business decisions, orchestration, and **transaction intent**
|
|
(the *what* of semantic atomicity — see §4).
|
|
- Depend on **feature-specific data/query/command interfaces**, never on a
|
|
generic repository and never on `DbContext`.
|
|
- Use `IOptions<T>` for typed configuration. Never inject `IConfiguration`.
|
|
- Keep feature responsibilities cohesive. Split a service when it has
|
|
independently changing business reasons — **not** because it crossed an
|
|
arbitrary line count (see §9, "no godfile threshold theater").
|
|
|
|
### 1.3 Data services
|
|
|
|
- Own EF Core query shape, persistence, batching, and **transaction mechanics**
|
|
(the *how* of commits — see §4).
|
|
- Accept `CancellationToken` on new I/O methods and propagate it (see §6).
|
|
- Use `AsNoTracking()` for read-only queries.
|
|
- Project only required columns for list/read models; include full graphs only
|
|
when the caller needs them.
|
|
- Page unbounded collections at the database.
|
|
- Avoid query-in-loop / save-in-loop patterns; prefer set-based reads and
|
|
batched writes **while preserving the workflow's failure and transaction
|
|
semantics**.
|
|
- Do **not** introduce a generic repository over EF Core. Feature data services
|
|
expose intent-revealing operations.
|
|
|
|
### 1.4 Infrastructure ownership and the EF allowlist
|
|
|
|
EF Core / `DbContext` is owned by approved infrastructure only. The explicit
|
|
allowlist of places that may touch EF directly:
|
|
|
|
- `SeaHaven.DataServices.Implementation.*` (feature data services).
|
|
- `Data.SeaHavenIndustries` — the `ApplicationDbContext`, entity configuration,
|
|
and **migrations** (see §7).
|
|
- `SeaHaven.DataServices.DependencyInjection.DataServicesModule` — context
|
|
registration and lifetime.
|
|
- Identity / authentication composition in `Program.cs` and the DI modules
|
|
(`AddDbContext`, `AddIdentity`, token/options wiring).
|
|
- Transaction mechanics inside data services when semantic atomicity requires
|
|
it (see §4).
|
|
|
|
Anything outside this allowlist that references EF / `DbContext` is an
|
|
architecture violation. New allowlist entries require an ADR (see §10).
|
|
|
|
## 2. Tenant scope and authorization
|
|
|
|
- **Tenant scope is server-derived.** Filtering by tenant/customer/owner is
|
|
applied in the data access layer from authenticated principal claims, never
|
|
from client-supplied request bodies or query strings.
|
|
- **Authorization remains at service entry.** A business service's public method
|
|
is the trust boundary: it authorizes the caller and resolves the effective
|
|
tenant scope before doing work. Controllers express HTTP-level authorization
|
|
(`[Authorize]`, route/policy) and then delegate; they do not re-implement
|
|
service-level authorization.
|
|
- Data services receive an already-resolved, server-derived scope; they must not
|
|
invent authorization rules of their own.
|
|
|
|
## 3. Dependency-direction enforcement (mechanical)
|
|
|
|
`ArchitectureTests` (in `Api.SeaHavenIndustries.Tests`) enforces mechanical
|
|
dependency direction by reflection over constructor parameters:
|
|
|
|
1. Controllers do not inject `DbContext`, concrete data services, or concrete
|
|
service implementations.
|
|
2. Business services do not inject `DbContext` or `IConfiguration`.
|
|
3. Business services depend on interfaces for their data/persistence
|
|
dependencies, not on concrete data-service or service implementations.
|
|
4. **Discovery must not silently pass on an empty set.** Each invariant asserts
|
|
the discovered type set is non-empty, so the gate **fails** if reflection
|
|
ever returns zero controllers/services (e.g., after a namespace move). A
|
|
passing run therefore means "real types were checked," not "nothing was
|
|
found."
|
|
|
|
These tests cover *dependency direction*. Behavior lives in behavior tests
|
|
through public interfaces. Do **not** add tests merely to prove a linter or
|
|
analyzer itself works — test the product behavior the rule protects. Do not
|
|
claim full runtime properties from constructor reflection; it proves wiring
|
|
shape, not execution behavior.
|
|
|
|
## 4. Transaction semantics and the one commit convention
|
|
|
|
- **Services own semantic atomicity.** A business service decides which set of
|
|
operations constitutes a unit of work and expresses that intent.
|
|
- **Scoped infrastructure owns transaction mechanics.** The actual commit
|
|
boundary is `DbContext.SaveChangesAsync(...)` and lives **inside a data
|
|
service**. A service must not sprinkle independent `SaveChanges` calls across
|
|
data-service boundaries in a way that breaks the semantic unit.
|
|
- **The repo documents one commit convention:** a single
|
|
`SaveChangesAsync(CancellationToken)` inside the owning data service is the
|
|
atomic commit boundary. This is the current convention observed across the
|
|
data-service layer.
|
|
- When a unit of work genuinely requires multiple statements to be all-or-
|
|
nothing, use EF Core's transaction (`IDbContextTransaction`) **inside the data
|
|
service**. Explicit transactions are infrastructure (EF allowlist), owned by
|
|
data services, never by controllers or business services.
|
|
- Do not "optimize" atomicity away: collapsing several semantically independent
|
|
saves into one final save changes partial-success behavior and is a business
|
|
decision, not mechanical cleanup.
|
|
|
|
## 5. Error disclosure and structured logging (security)
|
|
|
|
- **No HTTP response may expose** arbitrary exception messages, stack traces,
|
|
or SQL/provider/path/credential/crypto detail. Known failures are mapped to
|
|
explicit public messages; unexpected failures are funneled through the
|
|
centralized boundary (`SanitizedErrors`), which returns a stable message plus
|
|
a correlation reference and never echoes internal exception text.
|
|
- **Correlated structured internal logs** record the original exception at
|
|
`Error` level with a correlation id, so incidents are traceable without
|
|
leaking detail to the client.
|
|
- **Secrets are never logged.** Configuration values, tokens, connection
|
|
strings, and credentials are treated as redacted; structured log templates
|
|
must not interpolate secret-bearing fields.
|
|
- The `SanitizedErrorsTests` guard the public-facing contract (stable message,
|
|
correlation id present, original exception text absent, original logged at
|
|
`Error`).
|
|
|
|
## 6. Cancellation
|
|
|
|
- New async I/O methods accept a `CancellationToken` where the public contract
|
|
permits and **propagate it** to EF Core / downstream I/O.
|
|
- **Do not falsely claim propagation.** A `CancellationToken` parameter that is
|
|
ignored, swallowed, or replaced with `default`/`CancellationToken.None` at the
|
|
call site is a defect, not a courtesy. The signature is a contract.
|
|
- Enforcement combines **behavior tests** (assert the token reaches the data
|
|
service / EF call for changed I/O paths) and **analyzers** where available.
|
|
Either is acceptable evidence; silence is not. For changed paths, review must
|
|
confirm forwarding, not merely the presence of the parameter.
|
|
|
|
## 7. Database migrations
|
|
|
|
Migration gates require **actual-provider** evidence, not reflection or
|
|
assumptions:
|
|
|
|
- **Actual-provider discovery** — the migration provider is the configured
|
|
database provider (e.g., SQL Server / Npgsql), discovered from the runtime
|
|
configuration, not inferred from package references.
|
|
- **Snapshot consistency** — `ModelSnapshot` must be consistent with the model
|
|
after the migration set (`dotnet ef ... --output` / script diff with no
|
|
phantom operations).
|
|
- **Empty-db apply** — applying the full migration history to an empty database
|
|
must reproduce the current model with no errors.
|
|
- **Supported-prior upgrade** — upgrading from each still-supported prior state
|
|
applies cleanly.
|
|
- **No untested rollback claims.** Do not assert "down/rollback is supported"
|
|
unless a downgrade is actually exercised against the real provider; otherwise
|
|
state rollback as **not guaranteed**. EF "EnsureDeleted"/re-create is not a
|
|
migration rollback.
|
|
|
|
## 8. Query performance and complexity
|
|
|
|
For each changed hot path, document and test:
|
|
|
|
- input-size variables;
|
|
- CPU and memory complexity (Big-O) for any in-memory reconstruction;
|
|
- database round trips;
|
|
- whether filtering, ordering, and paging execute **in SQL** vs. in memory;
|
|
- whether tracking is necessary;
|
|
- whether repeated work can be hoisted out of loops;
|
|
- the partial-failure and transaction behavior.
|
|
|
|
Distinguish two classes of finding:
|
|
|
|
- **Analyzable anti-patterns** — query-in-loop / save-in-loop, unbounded load
|
|
followed by in-memory `Where`, repeated `First`/`Single` scans over a result
|
|
(prefer dictionary/hash-set reconstruction, normally `O(n)` vs `O(n²)`). These
|
|
can be flagged by inspection.
|
|
- **SQL-backed evidence** — actual paging/filtering/sort location, query count,
|
|
and plan shape must be backed by provider query logs, `ToQueryString()`,
|
|
store-generated SQL, or a plan/count measurement. Do **not** assert "executes
|
|
in SQL" or "single round trip" from LINQ shape alone.
|
|
|
|
Do not optimize by weakening correctness: batching all writes into one final
|
|
save can change partial-success behavior; adding a transaction can change
|
|
locking/retry semantics. Both are business decisions.
|
|
|
|
## 9. Size, complexity, and "no godfile theater"
|
|
|
|
- We do **not** enforce arbitrary line-count or file-size thresholds as quality
|
|
gates. A number is not an architecture.
|
|
- A file is too large when it has **independently changing responsibilities**
|
|
(low cohesion) or **untestable coupling**, not when it crosses a count.
|
|
Split on seams, not on a counter; describe the seam in the PR.
|
|
- Gates that operate on size must name the *consequence* (e.g., "two unrelated
|
|
features in one service") and be backed by review, not by a magic threshold.
|
|
|
|
## 10. Exceptions and ADRs
|
|
|
|
Some rules here are hard (EF allowlist, no client exception leakage, server-
|
|
derived tenant scope). A deviation from a hard rule requires an **Architecture
|
|
Decision Record** under `docs/adr/` (create the path if absent):
|
|
|
|
- A short `NNNN-title.md` stating context, decision, consequences, and the
|
|
specific rule being excepted.
|
|
- The excepted rule and an expiry/review date.
|
|
- Reference from the PR that relies on it.
|
|
|
|
Without an ADR, the rule stands as written. Explicit, documented exceptions
|
|
only — **no wildcard suppressions** (no global `[SuppressMessage]`, no
|
|
`.editorconfig` `dotnet_diagnostic.*.severity = none` sweeps, no
|
|
`#pragma` swaths). A suppression must name the single diagnostic and cite the
|
|
ADR/justification.
|
|
|
|
## 11. Configuration
|
|
|
|
Configuration is bound once during composition in
|
|
`SeaHaven.Services/DependencyInjection/ServicesModule.cs`. Business services
|
|
receive typed options (`FrontendOptions`, `JwtOptions`, `ApprovalsOptions`,
|
|
`VendorPortalOptions`, …). Defaults must preserve current behavior. Startup
|
|
validation is introduced only when every deployed environment is known to
|
|
provide the required value.
|
|
|
|
## 12. Review checklist (quick reference)
|
|
|
|
- Controller depends only on `I{Feature}Service` abstractions.
|
|
- Business logic is in a cohesive feature service.
|
|
- EF operations are behind a feature data-service abstraction.
|
|
- No raw `IConfiguration` in a business service.
|
|
- No unbounded load + in-memory filtering.
|
|
- No query/save loop where a set-based operation preserves semantics.
|
|
- Read-only EF queries use no tracking.
|
|
- New async I/O accepts and **propagates** cancellation where the contract
|
|
permits (verified, not just signed).
|
|
- Errors do not leak internal exception details; secrets are not logged.
|
|
- Tenant scope is server-derived; authorization enforced at service entry.
|
|
- Commit convention respected (single `SaveChangesAsync` in the owning data
|
|
service; transactions only in data services when required).
|
|
- Tests cover behavior, authorization, data contracts, ordering, duplicates,
|
|
missing records, and relevant failure boundaries.
|
|
- Changed behavior checked against the authoritative ticket board (see
|
|
`REVIEW_AND_PR_FRAMEWORK.md`). Missing board access blocks readiness/merge.
|