Skip to content

Coding Standards

Sergio Hernandez edited this page Jul 24, 2026 · 1 revision

Coding Standards

The rules a change must satisfy to be mergeable. Architecture explains why these patterns exist; this page is the checklist.


Dependency rules

Clean Architecture direction: Domain ← Application ← Infrastructure ← Web.

Allowed Web → {Infrastructure, Application, Domain}; Infrastructure → {Application, Domain}; Application → Domain; any layer → Common.* of the same or lower tier
Forbidden Domain → {Application, Infrastructure, Web}; Application → {Infrastructure, Web}; Infrastructure → Web

Forbidden patterns

  • DbContext usage in the Web layer — resolvers, endpoints, controllers.
  • Direct HTTP calls without IGraphQLClientFactory or a registered named HttpClient.
  • MediatR (the project uses the custom Common.Mediator) or AutoMapper (manual mapping only).
  • Non-async database operations, or a missing CancellationToken propagation.
  • Throwing a raw Exception — use NotFoundException, ForbiddenAccessException, ValidationException, TooManyRequestsException.
  • Service locator; static state outside DI; hard-coded connection strings or URLs.
  • Exposing domain entities directly in API responses — use records and DTOs.
  • GraphQL resolvers containing business logic — resolvers only dispatch via ISender.
  • new Guid(user.Id) / Guid.Parse(user.Id) — service-client tokens carry a non-GUID subject. Always Guid.TryParse and throw UnauthorizedAccessException on failure.
  • OrderBy / ThenBy applied AFTER projecting into a DTO or Vm record struct. Npgsql cannot translate an ordering whose key is a member of a constructor-built record struct; it throws at query time and surfaces as Unexpected Execution Error. Order on the entity columns inside the query expression, so the ordering survives a join. EF InMemory evaluates it client-side and the unit tests pass.
  • Distinct() / GroupBy() / set operations over an entity or projection containing a PostgreSQL json column (transporter_position.attributes) — json has no equality operator (error 42883). De-duplicate with EXISTS or key-based predicates.
  • Re-typing a GraphQL query string in a test — always reference the production internal const.
  • System.DateTime anywhere in src or tests — use DateTimeOffset.UtcNow.
  • [Caching] on a per-user or per-account query whose scope comes from IUser rather than the request.
  • New .js / .jsx files under TrackHub/src — an ESLint guard errors on them.

Required patterns

  • Commands and queries as readonly record struct : IRequest<TResult> with [Authorize(Resource, Action[, PrincipalTypes])] where authorization applies; handlers implement IRequestHandler<,>; validation via FluentValidation validators.
  • Reader/Writer separation — I{Entity}Reader / I{Entity}Writer in Domain/Interfaces, implementations in Infrastructure/{DbContext}, registered Scoped.
  • New by-id readers and writers extend the service's AccountScopedDataAccess base and call RequireAccountAccess / RequireAccountWriteAccess on the loaded row's owning account. Never an inline ad-hoc if.
  • EF configuration via IEntityTypeConfiguration<T>; BaseAuditableEntity for audited entities; domain events via BaseEvent + AddDomainEvent().
  • DI registration through a DependencyInjection.cs extension per layer. Every service's Application DI must include services.AddDistributedMemoryCache()CachingBehavior resolves IDistributedCache for every request type, and a missing registration fails every request with a masked DI error.
  • A service that uses [RequireFeature] must register its own IFeatureFlagService — Common's default is fail-open.
  • GraphQL server registration via AddTrackHubGraphQLServer<Query, Mutation>(...); per-service extras chain on the returned builder.
  • REST endpoints via EndpointGroupBase subclasses.
  • Inter-service GraphQL query documents as internal const string fields on the client class, with [assembly: InternalsVisibleTo("TrackHub.ServiceContracts.Tests")].
  • Service-identity calls require Security-side seeding: the client name in security.clients plus grants in security.service_client_permissions.
  • Paged queries return a …PageVm { Items, TotalCount } envelope, take int? Skip / int? Take, clamp through PageRequest.Clamp (default 50, max 500), and order by a human column then the primary key.
  • Lookup endpoints fetch through LookupLimits.FetchSize and validate with LookupLimits.EnsureWithinCeiling — fail loudly rather than truncate silently.
  • Delete mutations return the deleted entity's identifier, not a boolean.
  • Index names that would exceed PostgreSQL's 63-character limit get an explicit .HasDatabaseName(...).
  • Provider mappers convert to the canonical PositionVm units (speed km/h, decimal degrees, UTC DateTimeOffset) and carry a value-correctness unit test.

Naming

  • Assemblies and root namespaces: TrackHub.{Service}.{Layer} (repository folder names keep their historical spellings). Consumer clients: TrackHub.{Service}.Infrastructure.{Target}Api.
  • {Feature}{Action}Command|Query, {Name}Handler, {Name}Validator, {Entity}Reader|Writer, {Entity}Dto (input) / {Entity}Vm (output), ApplicationDbContext : IApplicationDbContext, endpoints {Feature} : EndpointGroupBase.
  • The cross-service telemetry attribute field is hourmeter everywhere. Provider-specific spellings (CommandTrack's hobbsMeter) are remapped only at the provider model, with JsonPropertyName.
  • Resource and action names come from Common.Domain.Constants — never string literals.

File organization

Application/{Feature}/Commands|Queries/{Name}.cs
Infrastructure/{DbContext}/{Entity}Reader|Writer.cs
Infrastructure/{DbContext}/Configurations/{Entity}Configuration.cs
Domain/Interfaces|Models/...
Web/GraphQL/Query|Mutation/{Feature}.cs
Web/Endpoints/{Feature}.cs
{Layer}/DependencyInjection.cs

One public class per file. Two IEntityTypeConfiguration<T> classes go in two files. Sanctioned exceptions: the CQRS feature file (a command plus its {Name}Handler and {Name}Validator), and grouped DTO/Vm contract files holding several related records.


New feature checklist — backend

  1. Domain entity in Domain/Models (inherit BaseAuditableEntity where audited).
  2. I{Entity}Reader / I{Entity}Writer in Domain/Interfaces.
  3. EF configuration + DbSet + migration (dotnet ef migrations add).
  4. Reader and writer implementations, registered Scoped, extending AccountScopedDataAccess where by-id.
  5. Command and query record structs with [Authorize], handlers, validators.
  6. GraphQL resolver methods (partial Query / Mutation) or an EndpointGroupBase for REST.
  7. If the feature adds or changes an inter-service call: extract the query to an internal const, add the contract test, and seed service-client permissions if the flow runs under a service identity.
  8. If the module ships a report: add the IReport (with GetDatasetAsync) in Reporting and a seeded Manager catalog row with its governance metadata, plus portal reportList / reportDescriptions i18n keys and a filter strategy.
  9. Run the repository's unit tests and dotnet test TrackHub.IntegrationTests/TrackHub.IntegrationTests.slnx.

New feature checklist — frontend

  1. Portal work is TypeScript (strict); new .js / .jsx under TrackHub/src is forbidden.
  2. Data access is layered — components never call the network directly: src/api/<backend>/<domain>Operations.ts (documents via the generated graphql() tag, values as variables only) → src/api/<backend>/<domain>.ts (typed functions that throw ApiError) → src/queries/<domain>.ts (TanStack Query hooks owning cache keys and invalidation). REST file endpoints go through src/api/core/restClient.ts; endpoint URLs live only in src/api/core/endpoints.ts.
  3. After any backend GraphQL surface change: run the contract tests (which re-export the SDLs), then npm run codegen. npm run typecheck && npm test && npm run build is the gate.
  4. i18n keys are compile-checked — add them to both locales/en.json and locales/es.json. Dynamic keys cast at the key expression only.
  5. Escape everything interpolated into a map popup. Leaflet bindPopup/bindTooltip and the Google InfoWindow assign via innerHTML, so React's escaping does not apply — use escapeHtml (src/utils/htmlUtils.ts).
  6. datetime-local ⇄ UTC goes through toDateTimeLocalInput / fromDateTimeLocalInput (src/utils/dateUtils.ts). Assert the round-trip property, never a literal.
  7. Argon components and controls export real prop interfaces — import and use them directly. Never re-introduce local prop-slice interfaces or as unknown as boundary casts at call sites; if a control lacks a prop you must pass, widen the control's exported prop type. Theme extensions live in src/types/mui-theme.d.ts.
  8. If the change adds or renames a screen, update the help topic's screens: frontmatter — build-help.mjs validates the mapping in both directions and will fail the build.

Dependency versions

  • TrackHubCommonTrackHubCommon/Directory.Build.props is the source of truth. Every consumer tracks the current version: the eight service repositories through Directory.Packages.props, and the ServiceContracts harness through a direct PackageReference that a props-only sweep misses. Bump every consumer together; never pin one back.
  • HotChocolate 16.5, ASP.NET Core / EF Core 10.0 across all repositories, including Tools.McpExtractor (which pins EF Core directly and must move with the stack).
  • Prefer latest. A newly introduced component is never pinned back; scope or disable a lagging tool instead.

Review policy

Before reporting a finding — code review, security review, audit — check the workspace's system-context/findings.md and system-context/rules.md. In particular:

  • Committed generic development secrets are intentional. Seeded OAuth client secrets, sample appsettings, .env examples and DBInitializer seed data are kept in-repository so the multi-service dev environment stands up without manual secret plumbing; production overrides them through environment variables. Do not flag these. Only flag committed real production or third-party credentials.
  • Entity placement, duplicated entity shapes across DbContexts, and the shared Manager/Geofencing database are documented decisions, not defects.
  • Open findings live only in findings.md. When you fix one, remove it there — and note the decision if it was accepted rather than fixed.

Keeping the context current

The workspace's system-context/ holds machine-generated catalogs alongside hand-maintained documents:

  1. dotnet run from tools/Tools.McpExtractor — regenerates root.json, database.json, api.json, domain.json, auth.json, dtos.json, common.json, topology.json.
  2. node tools/extract-frontend.js — regenerates frontend.json.
  3. Hand-update architecture.md, rules.md and findings.md.
  4. Verify the JSON parses and spot-check that new services and operations appear.

And update this wiki. It is the platform's technical documentation of record — repository README files carry only an overview, setup and gotchas.

Clone this wiki locally