to navigate Enter to open "…" all these words ANDOR to combine

Onboarding guide

Primer, the concepts, stack, and conventions you need first

This chapter teaches the cross-cutting things once, so the per-type chapters can stay focused. Read it before the group chapters (start with group-01). Everything here is either an architectural style the codebase commits to, an external dependency (BCL/NuGet, "external Level 0"), a build/language convention, or the architecture-evaluation lens the guide tags against. Later sections cross-reference back here.


1. The big picture

Two codebases are in scope:

  • MMCA.Common: a framework, published as seventeen NuGet packages (the count and list are owned by MMCA.Common/FACTS.md) to nuget.org (the documented install path) and mirrored to GitHub Packages (ADR-053) (four core: .Shared, .Domain, .Application, .Infrastructure; five presentation: .API, .Grpc, .UI, .UI.Maui, .UI.Web; three hosting: .Aspire, .Aspire.Hosting, .Gateway; four testing: .Testing, .Testing.E2E, .Testing.UI, .Testing.Architecture; plus the MMCA.Common metapackage that bundles the six a standard host always takes, ADR-101). It is not a runnable app; it ships the base classes and infrastructure for building modular monoliths with DDD + Clean Architecture + CQRS, plus the extension points to extract a module into its own microservice later. The packages release in lockstep (one version tags all of them).
  • MMCA.ADC: the Atlanta Developers Conference application, a consumer of those packages. It has modules (Conference, Engagement, Identity, Notification), a Blazor UI, a YARP gateway, and Azure infrastructure.

MMCA.ADC depends on MMCA.Common; MMCA.Common depends on neither. That is why the Common framework groups come first in this guide and the ADC business-module groups build on them, and within every group, the per-type sections run in ascending dependency Level.

The layered dependency flow (Clean Architecture)

MMCA.Common's own layering, enforced top-to-bottom (MMCA.Common/CLAUDE.md, "Architecture"):

API / Grpc        (presentation / transport)
     ↓
Infrastructure    (EF Core, caching, JWT, JWKS, outbox, message bus, SignalR)
     ↓
Application       (CQRS handlers, decorators, module system, IMessageBus)
     ↓
Domain            (entities, aggregates, domain events, specifications)
     ↓
Shared            (Result pattern, errors, DTOs, value objects)

Each layer references only layers below it, the dependency rule of Clean Architecture: source dependencies point inward, toward the domain, and the domain depends on nothing framework-specific. Two deliberate exceptions: UI and Grpc depend on Shared only, UI for Blazor WebAssembly compatibility, Grpc because it is pure transport that must not couple to business layers. The two host-support presentation packages sit above those: UI.Maui may reference UI and Shared only, and UI.Web (the Blazor Web host bridge) references UI, API, and Aspire, never Domain/Application/Infrastructure directly, which is why it disables transitive project references and carries its own boundary check (MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets:90-123).

MMCA.ADC repeats the same layering per module: each of Conference/Engagement/Identity has .Shared, .Domain, .Application, .Infrastructure, .API, and .UI projects following the same inward rule.


2. Architectural styles this codebase commits to

These are the recurring ideas. Each is taught fully at its first concrete appearance in a group chapter; here is the orientation so the vocabulary is familiar.

  • Domain-Driven Design (DDD). The model mirrors the business. Aggregates (a root entity plus the children it owns) enforce invariants inside their boundary; references between aggregates are by ID, not object graph. Value objects (Money, Address, Email) model concepts with no identity and are immutable. Domain events announce meaningful state changes. Factory methods return Result<T> so an invalid entity cannot be constructed. First concrete code: group-02 (ValueObject, IBaseEntity<T>) and group-04 (IDomainEvent).

  • Clean Architecture. See §1. The domain layer is free of EF/ASP.NET/serialization attributes; the application layer defines ports (interfaces) that infrastructure implements as adapters.

  • CQRS (Command/Query Responsibility Segregation). Writes (commands, which mutate and return a Result) are separated from reads (queries, side-effect-free). Both flow through a decorator pipeline: commands run FeatureGate → Logging → Caching → Validating → Transactional → handler, queries run FeatureGate → Logging → Caching → handler (no validation and no transaction on the read side). The order is set by the registrations in AddApplicationDecorators, which Scrutor's TryDecorate applies in reverse, so the last registered is the outermost (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:93-102). Cross-cutting concerns live in the pipeline, not in each handler. First concrete code: the ICommandHandler/IQueryHandler contracts and their decorators in group-05.

  • One shared HTTP middleware pipeline (ADR-079). The HTTP-side sibling of the decorator chain: every REST/gRPC service host builds its request pipeline from one UseCommonMiddlewarePipeline call, which fixes the middleware order once (exception handler through controllers) with the load-bearing adjacencies commented in code (authentication before the rate limiter and before tenant resolution, forwarded headers before both). Conditional middleware registers unconditionally and stays inert by config, so hosts differ by configuration, not by pipeline shape. Adopted by all seven ADC/Store service hosts, Helpdesk, and the template; the gateways and Blazor UI hosts sit deliberately outside it. First concrete code: group-12.

  • Vertical Slice Architecture. Within a module, a feature is a cohesive slice (command/query + handler + validator + DTO + mapper together), not scattered across horizontal Services/, Repositories/, Validators/ folders. Adding a feature means adding a slice.

  • Modular Monolith → extractable services. Modules implement a common IModule contract and are discovered and registered in dependency (topological) order by a module loader. Each module can later run as its own service host behind a YARP gateway without a rewrite, because application code talks to abstractions (IMessageBus, typed gRPC clients) and the transport choice lives at the edges. (ADRs 007 "gRPC extraction", 008 "service-extraction topology".)

  • Cross-service auth without shared secrets (ADR-004). Only the Identity service ever holds token-signing key material. The moment auth crosses a service boundary, tokens are signed asymmetrically (RS256) and every other service validates them via JWKS / OIDC discovery: AddForwardedJwtBearer points the bearer middleware at an authority, fetches /.well-known/openid-configuration, and follows jwks_uri to the published public key. So a compromised non-Identity service cannot mint tokens, and key rotation is publish-once at the issuer. The issuer is deliberately taken from the discovery document rather than pinned, because the internal service-discovery hostname differs from the public gateway origin, and both validators pin ValidAlgorithms so an attacker cannot force an algorithm swap. Downstream gRPC calls forward the already-validated JWT (ADR-007's JwtForwardingClientInterceptor). This is the auth half of the extraction promise in the previous bullet: token validation survives the issuer/validator split without a rewrite. Adoption note (verified by source): the guarantee is scoped to cross-service auth. The in-process monolith default stays HS256 with a shared symmetric secret behind the same JwtSettings.SigningAlgorithm switch, and MMCA.Helpdesk (the monolith seed) runs issuer-less. ADC's Conference, Engagement, and Notification services all validate through the gateway-routed discovery path today. First concrete code: group-08.

  • Write-once UI, render everywhere (Blazor + .NET MAUI Hybrid). A UI page is authored once as a Razor component in a per-module Razor Class Library (MMCA.ADC.{Module}.UI, e.g. Conference.UI's EventList.razor/EventDetail.razor). Both the web host (MMCA.ADC.UI.Web / .Web.Client, Blazor Server + WebAssembly) and the .NET MAUI host (MMCA.ADC.UI) ProjectReference the same UI libraries, so one page renders across Web, Android, iOS, macOS, and Windows with no per-platform reimplementation, MAUI hosts the shared components in a BlazorWebView (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MainPage.xaml:16, wired by AddMauiBlazorWebView() in MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/MauiProgram.cs:64). The only platform-specific code is tiny entry points (App/AppDelegate/MainApplication, MauiProgram). First concrete code: the MAUI bootstraps and host shells in group-25; the supported device/browser matrix is in MMCA.ADC/CLAUDE.md.

  • Event-driven integration + the Outbox pattern. When an aggregate changes, its domain events are serialized into an OutboxMessage row in the same transaction as the data, then a background processor delivers them at-least-once. This avoids the "save then publish and hope" dual-write bug. (ADR-003 "outbox dual-dispatch".)

  • Database-per-service. Each module/service owns its own SQL database and its own outbox table; there is one concrete SQLServerDbContext class but one instance per database. Cross-source relationships auto-degrade (the FK is dropped, navigation flows through batch loaders), and the outbox is the cross-source consistency mechanism. (ADR-006.)

  • Engine-agnostic entities, the storage provider is a one-token choice (ADR-018). A domain entity carries no persistence-engine choice; it is a plain class. What decides whether it is stored in SQL Server, Cosmos DB, or SQLite is a single [UseDataSource(<engine>)] attribute on its Infrastructure/Persistence/EntityConfiguration/{Entity}Configuration class, carried for you by one of three thin engine shim base classes (EntityTypeConfigurationSQLServer<TEntity, TId>, …Cosmos<…>, …Sqlite<…>). All three derive from a single engine-aware EntityTypeConfiguration<TEntity, TId> base (which reads the attribute and applies the matching table/container/schema/key conventions) over EntityTypeConfigurationBase<TEntity, TId>, group-07. So swapping just that base (or attribute) re-points the same entity to a different engine with no configuration-body edits and zero change to the domain, application layer, or entity, the engine is resolved up front by the EntityDataSourceRegistry, the right DbContext is built per data source, cross-source relationships auto-degrade, and a cross-source filter goes through CrossSourceSpecification (so even a "published-event" predicate stays translatable). First concrete code: the configuration hierarchy in group-07; this is the per-entity half of database-per-service (ADR-006) plus the polyglot story (ADR-018). Adoption note (verified by source): this is a real, tested capability that no entity routes to yet. Today every entity configuration in ADC derives from the …SQLServer base (ADC runs SQL Server only, four databases: ADC_Identity, ADC_Conference, ADC_Engagement, ADC_Notification, MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:32-35), and the ADR-018 work shipped the full polyglot machinery (unified base, cross-source spec + fitness rule, Cosmos-index skip, SQLite EnsureCreated, Cosmos/SQLite Aspire helpers, portability tests). An end-to-end trial moving ADC Conference's Session to Cosmos and Room to SQLite was built and tested locally, then deliberately reverted to all-SQL-Server with every framework extension point kept. Treat Cosmos/SQLite as supported, exercised extension points, see the coverage audit's extension-point inventory.

  • The Result pattern. Expected error paths use a Result/Result<T> return value carrying Errors, not exceptions. This is the single most pervasive idiom in the codebase, taught in full in group-01 (ErrorType, Error, Result).

  • Soft-delete + audit fields. Entities are never hard-deleted; an IsDeleted flag plus EF global query filters exclude them. CreatedOn/By and LastModifiedOn/By are stamped centrally in SaveChangesAsync. For genuine erasure (GDPR/CCPA) there is a separate anonymize path. (ADR-005.) Since ADR-075 the same idea extends to an opt-in field-level audit trail: a third SaveChangesInterceptor, registered last so it diffs freshly stamped values, writes per-property AuditTrailEntries in the same transaction as the data, with [Pii] values captured redacted (opt-in twice: AddAuditTrail plus an IAuditedEntity marker per entity; retention is an ADR-074 scheduled purge).

  • Multi-tenancy as a persistence-layer commitment (ADR-073). Tenancy is not per-handler filtering: a second named EF query filter, "Tenant", composes by AND with the "SoftDelete" filter and embeds the executing tenant as a SQL parameter, so one cached model per source serves every tenant (a null tenant is the system context and sees all). ITenantContext resolves claim-then-header behind TenantResolutionMiddleware, a dedicated SaveChangesInterceptor stamps writes and refuses cross-tenant ones, the outbox drains per (source, tenant) pair, the caching decorators prefix keys with the tenant, and DB-per-tenant is a per-tenant connection-string override under the same DataSourceKey. Adoption note (verified by source): like the polyglot machinery above, this is a real, tested capability with one reference adopter: MMCA.Helpdesk runs it end to end, while ADC and Store stay single-tenant (the filter is inert without opt-in). First concrete code: group-07 (filters, interceptor) and group-12 (resolution middleware).

  • Primitive identifier type aliases (ADR-048). Each entity's ID type is a per-module global using XIdentifierType = int; (or = System.Guid;) alias, linked into every project via Directory.Build.props. Code says EventIdentifierType, not bare int, so the ID type can change in one place: ADC's SpeakerIdentifierType is a System.Guid beside fourteen int siblings in the same file (MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:18, in MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/). These are aliases, not wrapper structs: the alias is erased at compile time, so it buys readability and one-place change, not compile-time protection against passing the wrong same-typed ID. (The identifier-alias mechanism is covered with the entity contracts in group-02.)

The decision records (ADRs) this guide tags

The why behind these patterns lives in the accepted ADRs (the Website repo's docs-src/adr/README.md owns the count and range), version-controlled in Website/docs-src/adr/ and published at https://ivanball.github.io/docs/adr/ (its README.md is the canonical index with one-line summaries). Group chapters tag the relevant one inline (e.g. "ADR-003"); the full set, for orientation:

ADR Decision (one line) First/most relevant chapter
001 Manual DTO mapping (Mapperly), not reflection-based AutoMapper g12
002 INavigationPopulator<T> for cross-container/cross-source eager loading g11
003 Outbox + in-process dispatch + background processor (at-least-once); since ADR-100 the outbox is resolved from the messaging mode (MessageBus:EnableOutbox is bool?, unset means enabled iff the provider is not InProcess) g04
004 JWKS discovery + fallback for cross-service token validation g08
005 Soft-delete for lifecycle; IAnonymizable + outbox purge for GDPR/CCPA erasure g02/g24
006 Database-per-service: each owns its DB + outbox; one SQLServerDbContext class, one instance per DB g07
007 *.Contracts + typed gRPC clients + Result-over-the-wire for synchronous inter-service calls g13
008 One service host per module behind a YARP gateway; transport at the edge keeps extraction reversible g16/g25
009 Standard resilience handler on every outbound client; declared RTO/RPO + drilled restore g13/devops-runbooks
010 Every integration event carries a SchemaVersion; breaking changes use a new type + upcaster g04
011 en-US-only i18n is a deliberate non-goal superseded by ADR-027 (multi-locale en-US + es) g15
012 gRPC host transport: both consumers now default to Http2-only h2c (Profile A); Http1AndHttp2 survives on the WebSocket hosts only (ADC Notification, which adds a dedicated Http2 gRPC endpoint beside it, and Store Sales) g16/g20
013 Expected failures are Result/ErrorType values; only the edge maps to HTTP/gRPC g01
014 CQRS decorator chain: FeatureGate → Logging → Caching → Validating → Transactional → Handler g05
015 Architecture fitness functions: compile-time layer guard + shared NetArchTest rule library g27
016 Lockstep versioning of every package (one tag, one version, no phased rollout); MassTransit pinned to v8 (build-gated) devops-cicd
017 [Idempotent] action filter dedups client retries via an Idempotency-Key header (24h replay) g12
018 Polyglot persistence: three engines (SQL Server / Cosmos / SQLite) behind one entity model, engine via [UseDataSource] g07/g03
019 Layered rate limiting: an always-on global limiter caps only authenticated callers; anonymous/infra traffic is exempted, with output cache + login-protection for the other layers g08/g12
020 Permission-based authorization: [HasPermission(...)] over an IPermissionRegistry, opt-in atop RBAC g08
021 Consumer-side inbox idempotency: EfInboxStore dedups broker redeliveries by MessageId g04
022 Browser session-cookie auth: HttpOnly cookies + a non-validating SSR scheme so [Authorize] passes on prerender g08
023 Security-response headers + pluggable CSP (ICspPolicyProvider); the baseline CSP omits script-src/style-src so it cannot break Blazor g16/g25
024 Two-channel notifications: a durable UserNotification inbox and a transient SignalR push, behind IPushNotificationSender g10
025 Startup warm-up + readiness gating: WarmupHostedService + a ready-tagged WarmupReadinessGate hold /health/ready until warm g16
026 Two-tier caching: a swappable ICacheService substrate (Memory/Redis) + an HTTP output-cache edge tier; amended 2026-09-01 with an optional third, client-side tier (IUiReadCache, a per-circuit read-through cache over the API client, registered by AddUIShared but opt-in per UI service and adopted by no consumer today) g09/g15
027 Multi-locale i18n (supersedes 011): en-US + es via .resx/IStringLocalizer; backend errors localized at the edge by Error.Code g12/g15
028 Day/Dark theme: ThemeService binds MudThemeProvider's IsDarkMode, persisting cookie/localStorage/PreferredTheme g15
029 Auth brute-force protection: ILoginProtectionService throttles the anonymous surface (email-keyed lockout + per-IP registration cap) g08
030 Startup sole-migrator: each service self-applies its EF migrations at boot (DatabaseInitStrategy=Migrate), no sqlcmd backstop g07/g12
031 Feature-flag management: [FeatureGate] (404) + the IFeatureGated decorator for one config-driven flag name g12/g05
032 Password hashing: PBKDF2-HMAC-SHA512 (600k iters) with by-salt-length migration of legacy records superseded by ADR-102 (PBKDF2-only, the legacy branch deleted) g08
033 Resource-ownership authorization: OwnerOrAdminFilter/OwnershipHelper row-scope a single resource beside RBAC g08
034 Generic entity controllers + dynamic query contract (EntityControllerBase; fields/filter/sort/paging); the write side (generic create/update/delete, CrudEntityControllerBase) is completed by ADR-099 g12/g03
035 Optimistic concurrency: a RowVersion token round-trips through IConcurrencyAware DTOs; a stale write maps to HTTP 409 g07/g12
036 External OAuth login (Google/GitHub): OAuthControllerBase swaps a single-use 2-minute code for the local JWT pair (tokens never ride the redirect URL) g08/g12
037 Field-level encryption at rest: EncryptedStringConverter (AES-256-GCM), shipped + tested but unadopted (no entity config wires it yet) g07
038 Supply-chain provenance: SBOM release gate + committed lock files + transitive vuln audit + packageSourceMapping devops-cicd
039 Live channel push: hub JoinChannel/LeaveChannel groups + ILiveChannelPublisher publish ephemeral events over the one notification WebSocket g10/g15/g23
040 Authenticated output caching for public reads: PublicEndpointOutputCachePolicy stops a Bearer header from bypassing the output cache on [AllowAnonymous], user-independent GETs g12
041 Observability strategy: shared OTel baseline + CQRS RED histograms + outbox dead-letter counter + correlation middleware, with head-sampling and poll-span-filter cost knobs g16/devops-aspire
042 Device capability abstraction (MAUI Blazor Hybrid): per-capability contracts + TryAdd null/browser fallbacks + MAUI-native overrides, IDeepLinkDispatcher; MMCA.Common.UI.Maui is the one MAUI-TFM package, built outside the main solution g26
043 Mobile deep links + app association + native OAuth callback: allow-listed custom-scheme redirect of the single-use code; assetlinks.json/AASA served by the UI.Web host g12/g26
044 Native push delivery (third channel, amends 024): INativePushSender/IPushDeviceRegistrar (Azure Notification Hubs, Null defaults) reach backgrounded/killed apps; non-fatal after the inbox+SignalR legs g10/g07
045 Managed file storage + avatars: IFileStorageService (Azure Blob/Null) + IImageProcessor (crop, strip metadata, re-encode); 2 MB in, 256x256 JPEG out, [Pii] URL nulled on anonymize g07/g24
046 HTTP API versioning: one AddCommonApiVersioning (header api-version, default 1.0); ServiceInfoControllerBase v1.0-deprecated + v2.0 exemplar, fitness-asserted per repo g12/g20
047 Soft-deleted-user session revocation: SoftDeletedUserMiddleware 401s an authenticated caller whose User.IsDeleted, via a 30s-cached ISoftDeletedUserValidator, bounding the stateless-JWT revocation window g12
048 Primitive identifier type aliases: entity IDs are primitives behind per-module global using {Entity}IdentifierType, chosen over strongly-typed ID structs (readability + zero EF/serializer friction) g02/g14
049 Library-scoped ConfigureAwait(false) policy: packaged non-UI framework code is build-gated (CA2007 warning for Source/** in Common's .editorconfig delta, UI packages excluded); protects the MAUI head and any non-ASP.NET consumer from context-capture deadlocks devops-cicd
050 JWT + single rotating refresh token: a short-lived stateless access token plus one server-stored opaque refresh token per user that rotates on every use (mismatch/expiry revokes + 401); the sliding expiry re-stamps on rotation, and single-token-per-user signs other devices out superseded by ADR-097 (rotation, reuse detection and sliding expiry survive over per-device rows hashed at rest) g08
051 Client-side auth token lifecycle across render modes: one ITokenRefresher with two head strategies (browser heads refresh through the same-origin proxy per ADR-022; MAUI refreshes directly and persists the rotated pair in OS SecureStorage) g15/g25
052 Background job execution: work outliving a request runs as a bounded Channel<T> plus a SingleReader hosted drain, never an untracked Task from a controller, so the host can cancel and await it on shutdown; full mode encodes what the work is worth (DropOldest vs Wait) g23
053 Dual-registry package publishing: one tag pushes the same nupkgs to nuget.org (the documented install path) and GitHub Packages (mirror), authenticated keylessly through GitHub OIDC trusted publishing rather than a stored API key devops-cicd
054 Saga compensation + reconciliation backstop: each workflow step raises a domain event and its compensating action runs in its own handler and DI scope, committing after the originating transaction; idempotency is a persisted aggregate marker written by the same SaveChanges as the compensating writes g04
055 Repository + Specification contract: the read side is ISP-split into IEntityReader (id lookups) and IEntityQuerier (collections, projections, counts), and a fitness rule fails the build on raw IQueryable surfaces in Application code g03/g07
056 One render mode for the whole routable tree, chosen at the root on the shared Routes component: InteractiveAuto with prerendering left on, and the resulting SSR-to-interactive double fetch removed in DataGridListPageBase through PersistentComponentState rather than by weakening the mode g15
057 Expand/contract schema evolution enforced in CI: adding columns, tables and indexes is safe in any release, while a migration added by a PR whose Up() drops one fails the merge check unless it carries an EXPAND-CONTRACT-OVERRIDE marker, because production rollback is revision-only and never un-migrates devops-cicd/g07
058 Runtime conformance suites shipped in MMCA.Common.Testing: six abstract behavioral bases (problem details, OpenAPI, /ServiceInfo versioning, security headers, graceful shutdown, decorator order) that a host subclasses and that run against a really booted host, picking up where ADR-015's structural fitness tests stop g27
059 IModule is the one composition contract (five members, three defaulted): reflection discovery over the AppDomain, Kahn topological registration order, and a disabled module represented by null-object stub registrations rather than by absence g14
060 Performance-regression gate: a performance-smoke job runs the BenchmarkDotNet suite on every code PR and verifies it against a committed perf-baseline.json of absolute allocation ceilings plus benchmark-to-benchmark ratio floors; no absolute wall-clock threshold, since a shared runner cannot deliver one devops-cicd
061 Runtime secrets live in Azure Key Vault and reach each Container App as a keyVaultUrl reference resolved by one shared user-assigned managed identity, consumed only through secretRef (no inline values); SQL managed-identity auth is staged behind useManagedIdentitySql, still false by default devops-iac
062 SLO alerting as code: sloAlertSpecs in each consumer's Bicep materializes KQL scheduled query rules (401/499 and hub traffic excluded) on one action group, and a framework test base pairs every alert with a severity-correct OPERATIONS.md triage section, failing the build in either direction devops-iac/g27
063 WCAG 2.1 AA as a shipped test contract: AxeOptions.Wcag21Aa pins the four WCAG tag sets and excludes axe's advisory rules, a violation throws instead of reporting, and the scan is a cross-browser required merge check in Common plus a chromium deploy gate in both apps g27/devops-cicd
064 Deploy preconditions as proof of recency: dr-freshness, load-freshness and cross-service-freshness sit in deploy.needs and fail when the newest successful DR drill (8 days), k6 load run (35) or broker round-trip (5) is older than its window; no successful run at all fails too devops-cicd
065 Scaffolding templates derived from the reference app: the dotnet new pack MMCA.Templates (mmca-app/mmca-module/mmca-command/mmca-query) is staged at pack time from the MMCA.Helpdesk tree itself (no second copy to drift); generated apps ship build/add-module.ps1, which performs the seven wire-ups the template can only print, plus the first migration; a template-smoke CI job builds a generated app package-mode and sweeps for residual Helpdesk/Ticket tokens g14/devops-cicd
066 Broker transport selection + dev/prod parity: one IMessageBus with three MessageBusProvider values (InProcess for tests and the monolith, RabbitMQ wired by the AppHost's WithBroker locally, Azure Service Bus injected by both apps' Bicep in production), identical exponential retry per transport, and a non-gating Service Bus emulator test tier proving the production binding g04/g16
067 Shared Blazor shell + IUIModule composition: the framework package ships the router, layout, nav menu and routable shell pages; each module contributes NavItems, an Assembly for AdditionalAssemblies route discovery, and app-bar/layout extension points, enumerated by Routes.razor at runtime (the UI-layer counterpart of ADR-059's IModule; Helpdesk keeps its own shell) g15/g25
068 Value objects as validated domain primitives: seven sealed record types over an abstract ValueObject base, each a private constructor plus a Result-returning Create factory (fitness-enforced), mapped via OwnsMoney or value converters (no schema change); the deliberate opposite of ADR-048's identifier aliases (identifiers cross boundaries, domain values carry invariants) g02
069 Shared DataProtection key ring for scaled-out hosts: AddCommonDataProtection persists the key ring to one Azure blob under DefaultAzureCredential so cookies and antiforgery tokens minted by one replica decrypt on another; Key Vault at-rest encryption is a deliberately independent second gate, and absent config is a full no-op (adopted by ADC; Store still runs per-replica key rings) g16/g08
070 Fail-fast configuration contract: every settings section binds through AddOptions().Bind().ValidateDataAnnotations().ValidateOnStart() so a misconfigured host refuses to boot instead of failing at first use; settings consumed above Infrastructure flow through read-only singleton facades (IApplicationSettings, ISmtpSettings, IJwtSettings) rather than IOptions<T> g12/g14
071 Barcode scanning + QR display, split by what each depends on: QrCodeImage is a plain shared component (QRCoder PNG as a data URI, no device needed), while camera reads go through IBarcodeScannerService, an ADR-042 capability (never throws, null = cancelled/denied/unsupported) with a TryAdd null fallback and a ZXing.Net.MAUI implementation, opt-in per head g26/g15
072 QR badge check-in + points gamification (ADC): the badge QR carries an opaque server-verified Guid (revocable by one Regenerate()), one CheckIn aggregate with Event/Session/Sponsor scopes behind filtered unique indexes, and an append-only points ledger whose unique (UserId, ActivityType, SubjectKey) index is both the redelivery-idempotency guard and the anti-farming rule g22/g17
073 Multi-tenancy (shared-schema + DB-per-tenant): a second named EF query filter "Tenant" composes by AND with "SoftDelete"; ITenantContext resolves claim-then-header behind TenantResolutionMiddleware, a dedicated interceptor stamps writes and refuses cross-tenant ones, and DB-per-tenant is a per-tenant connection-string override (Helpdesk is the reference adopter) g07/g12
074 Recurring job scheduler: durable multi-replica-safe cron built on the outbox claim-lease idiom (not Hangfire/Quartz); IScheduledJob resolved scoped per execution, a ScheduledJobEntry store on the Default source, Cronos parsing, smart-wait runner via TimeProvider; missed schedules run once then advance g04
075 Audit trail: a third SaveChangesInterceptor (registered last, so it diffs freshly stamped values) writes per-property AuditTrailEntries in the same transaction as the data; opt-in twice (AddAuditTrail + IAuditedEntity per entity), [Pii] values captured redacted, retention via an ADR-074 scheduled purge g07
076 Data-subject export (DSAR) contract: ExportUserDataHandlerBase mirrors the delete handler's ownership gate, fans out to registered IUserDataExportSections, and assembles a versioned JSON export; per-section failure degrades to Available = false rather than failing the request, because a DSAR is a legal deadline g24/g12
077 HybridCache substrate (amends 026): AddCommonHybridCache swaps ICacheService to L1 in-process + L2 distributed under a disjoint {prefix}hc:{key} keyspace, making the two-serialization-formats-in-one-keyspace failure impossible rather than unlikely; IncrementAsync bypasses L1 to keep counter semantics g09
078 CSV export as a dedicated [HttpGet("export")] endpoint on EntityControllerBase, not content negotiation (the output cache does not vary by Accept); page-loops the capped query pipeline and streams up to MaxExportRows, truncating with an X-Export-Truncated header; RFC 4180 writer in-house g12/g03
079 Shared HTTP middleware pipeline: UseCommonMiddlewarePipeline fixes one middleware order for every REST/gRPC host (exception handler through controllers), with the load-bearing adjacencies commented in code; conditional middleware registers unconditionally and stays inert by config (the ADR-014 decorator-order sibling, for the HTTP side) g12
080 Rollout + automatic revision rollback: both consumer deploys end in a post-deploy smoke gate asserting expected status codes; on failure every container app walks back to its previous revision (az containerapp revision copy); rollback is revision-only by construction, schema is never reverted (ADR-030/057) devops-cicd
081 Cost baseline as a deploy gate: a read-only cost-guard workflow in deploy.needs asserts the production footprint still matches its baseline (replica caps + accepted SQL tiers), so an un-reverted manual surge blocks the next deploy; it never mutates anything devops-cicd/devops-iac
082 Two-tier cross-origin posture: service hosts get named allow-listed CORS policies from one AddCommonCors (origins from config, empty by default), selected inside the shared pipeline; the gateways get a default policy restricting only origins, because a reverse proxy must forward arbitrary client headers g12/g16
083 CRUD lifecycle event taxonomy: one EntityChangedEvent<TId> base (a DomainEntityState discriminator + the entity id) replaces per-entity Created/Updated/Deleted triples; business state-machine transitions deliberately keep their own event types, and the discriminator rides integration events as a frozen wire field g04/g02
084 Stripe webhook ingress contract (Store Sales): an anonymous raw-body POST verified by Stripe-Signature whose status code encodes ACCEPTED-vs-PROCESSED, not success/failure; 400 only when the event cannot be accepted at all, because rejections make Stripe retry and eventually disable the endpoint; post-acceptance failures log and return 200 with ADR-054's reconciliation as backstop g04/g12
085 Identifier type aliases revisited (revisits 048): the wrapper-struct alternative is re-evaluated, priced (43 aliases, 42 of them int; 3,641 occurrences across 1,016 files to migrate) and deferred again, now behind three named revisit triggers instead of an open-ended "not now" g02/g14
086 Process manager deferred (relates to 054): a documented deferral shipping no code; records the shape a durable coordinator would take (MassTransit v8 saga state machine, per-instance state + deadlines) and the build trigger (3+ steps across 2+ services, state that fits no aggregate, a per-instance deadline); ADR-054's compensation + sweep suffice until then g04
087 Broker poison-message handling (amends 009): transport-aware second-level redelivery (opt-in on RabbitMQ, native on Azure Service Bus), an auto-registered FaultIntegrationEventConsumer<TEvent> that makes an exhausted message visible but never replays it (meter MMCA.Common.Broker), and a circuit breaker on the outbox broker publish only; a per-query DB breaker is rejected (does not compose with EF's execution strategy) g04/g07
088 Gateway edge responsibilities (extends 019): the edge owns three cross-cutting behaviors via MMCA.Common.Aspire: GatewayCorrelationMiddleware ensures + forwards X-Correlation-ID, per-client-IP rate limiting that deliberately includes anonymous callers (inverting ADR-019's exemption), and downstream health probes on the Ready tag only; edge JWT pre-validation is declined with a trigger g16
089 Gateway topology owned by configuration (amends 008): the route table moves out of MapForwarder code into YARP ReverseProxy configuration as the single route source, with RouteMapTests as a drift gate in both consumers and the per-destination HTTP version policy (ADR-012 profiles) in cluster config; the AppHost/bicep keep address books, not route tables g16
090 Event upcaster registration extension point (completes 010): IEventUpcaster<TSource, TTarget> in the Application layer plus an EventUpcasterRegistry that chains V1 to V2 to V3 to the terminal contract and re-stamps MessageId/DateOccurred after every hop, so inbox dedup survives upcasting; both delivery paths consult it, and a duplicate/self-map/cycle throws at host start g04/g05
091 Cache-backed password reset (extends 029/032): the reset credential is one ICacheService record (256-bit token, only its SHA-256 stored, 30-minute TTL, single live token per address, 5 validation attempts, 3 requests per 60 minutes) rather than three columns on the user row, and ForgotPasswordHandlerBase returns success on every path so the endpoint is not an account-enumeration oracle g08/g14
092 Core Web Vitals budget as a shipped test contract and deploy gate: WebVitalsCollector installs PerformanceObserver hooks as a Playwright init script, WebVitalsBudget defaults to the good band (LCP 2500, FCP 1800, TTFB 800 ms, CLS 0.1, INP 500), a breach throws naming the page, and both apps' assertions ride the chromium e2e-gate g27/devops-cicd
093 Container image build posture: eleven four-stage Dockerfiles where the GitHub Packages token is a BuildKit secret (never an ARG/ENV), there is deliberately no separate dotnet build stage (publish re-restores; the RID split made every image compile twice, about 75 s), and PublishReadyToRun=true on the nine service/gateway images only; floating base tag and running as root are recorded as undecided devops-aspire/devops-cicd
094 Client-side entity data-access contract (the calling half of 034): hand-written typed bases in MMCA.Common.UI (AuthenticatedServiceBase + EntityServiceBase<TEntityDTO, TIdentifierType>), no generated client; the user-facing Polly retry lives in the client base rather than in ADR-009's resilience handler, and ADR-017's Idempotency-Key is minted client-side for creates only and held constant across the retry burst g15/g12
095 Uniqueness under soft delete: SoftDeleteUniqueIndexConvention, registered once in ApplicationDbContext.ConfigureConventions, filters every unique index on a non-owned IAuditableEntity to live rows, so a deleted record stops occupying its unique slot forever; a hand-authored filter wins, HasSoftDeleteFilter is the manual extension point, and Cosmos is a no-op g07
096 Best-effort side-effect contract: one BestEffort.ExecuteAsync(operation, logger, action, ct) helper awaits the side effect and turns any failure into exactly one Warning plus one besteffort.dispatch.failed increment on its own MMCA.Common.BestEffort meter; caller cancellation is rethrown rather than swallowed, and the operation name stays a low-cardinality constant g03/g22
097 Multi-device refresh sessions (supersedes 050): refresh tokens become rows in a RefreshSessions table, one per signed-in device, stored as an unsalted SHA-256 hex digest (every lookup is by hash and the input is 64 bytes of CSPRNG output), chained on rotation via ReplacedByTokenHash; RefreshSession is a flat framework record (no audit stamps, no soft delete, no concurrency token, because a revoked row must stay findable for the reuse check), IRefreshSessionStore is the persistence contract and RefreshSessionCleanupService sweeps expired rows g08/g07
098 Aspire for orchestration, not for testing or production dashboards: each AppHost composes the local stack and every host calls AddServiceDefaults/MapDefaultEndpoints, but integration testing stays WebApplicationFactory + Testcontainers (SqlServerIntegrationTestFixtureBase<TEntryPoint> per service, CrossServiceFixtureBase for the three-host tier) and DistributedApplicationTestingBuilder stays out; production observability is Application Insights, not the Aspire dashboard devops-testing/g27/devops-aspire
099 Generic write-side entity commands (the half 034 left at create and delete): the module writes one IEntityUpdateApplier<TEntity, TUpdateRequest, TIdentifierType>.ApplyAsync that calls the aggregate's own guarded methods, so invariants and events keep exactly one home; UpdateEntityCommand carries id, request and RowVersion and reuses the existing concurrency/idempotency markers, MutateEntityHandlerCore hosts the shared load-apply-save sequence, and CrudEntityControllerBase exposes it over HTTP g05/g12
100 Outbox resolved from the messaging mode (amends 003, on the rule 021 set for the inbox): MessageBus:EnableOutbox is bool?, IsOutboxEnabled resolves unset as Provider != InProcess, an explicit value wins in both directions; on the disabled path AddInfrastructure registers neither OutboxProcessor nor OutboxCleanupService and adds OutboxDisabledNoticeService instead, the schema is kept, and the one combination that cannot work (a broker with the outbox off) is refused at startup g04/g14
101 MMCA.Common metapackage (the Core 6): one PackageReference in place of the six a standard host always takes (Shared, Domain, Application, Infrastructure, API, Aspire), ordered so the bundle reads as the architecture it installs; it ships no assembly (IncludeBuildOutput=false, NU5128 suppressed in that project alone) and is versioned by MinVer off the same tag, so it releases in lockstep to both registries devops-cicd
102 PBKDF2-only password hashing (supersedes 032): one IPasswordHasher, one PasswordHasher implementation registered with TryAddSingleton (stateless, three private constants), PBKDF2-HMAC-SHA512 with a 32-byte salt, 64-byte digest, 600,000 iterations and a FixedTimeEquals compare; VerifyPassword has no branch, so the legacy HMAC salt-length selection and every HMACSHA512 usage are gone from Source/ in all four repos g08
103 bUnit component-test tier as a package: MMCA.Common.Testing.UI ships BunitComponentTestBase, which fixes once the choices every consumer UI test tree would otherwise re-derive (bUnit v2 on the xUnit v3 / Microsoft Testing Platform line, BunitContext and Render<T> isolated behind RenderUnderTest/RenderAs, the MudBlazor service set and the JS-interop stubs), so a move off that bUnit line changes one file g27
104 Plain enums by default, Enumeration<T> opt-in: a bounded set is a plain C# enum unless a member must carry data or behavior; the shipped smart-enum base discovers public static readonly members once by DeclaredOnly reflection, freezes per-type value and case-insensitive name lookups, returns Result from FromValue/FromName (013), uses type-guarded equality that deliberately declines IEquatable<T>, and does not derive from ValueObject g02
105 Data residency as a build gate: DataResidencyTestsBase (one [Fact], rubric section 30) parses the region where a repo actually provisions PII-bearing storage from that repo's own infrastructure source of truth (ADC from the SQL_LOCATION_OVERRIDE default in deploy.yml, Store from its DR runbook) and fails the build unless PRIVACY.md states it, with a per-repo denylist that blocks a stale or copied region claim g27/devops-cicd
106 Extension members as the public DI surface: the framework's entire Add* registration surface is written as C# extension(T) blocks inside static classes under LangVersion preview in all four repos (82 blocks in 65 files, 23 of them extension(IServiceCollection services), measured 2026-09-01); the call site is indistinguishable from a classic extension method and the compiler-emitted classic static method keeps the choice reversible, with the public-API baselines recording both shapes g14
107 ExecuteInTransactionAsync is a re-entrant, retriable, commit-once unit: an inner call joins the ambient transaction instead of nesting, the whole delegate retries under EF's execution strategy with a change-tracker reset per attempt, a failed Result rolls back exactly like a throw, and a commit whose outcome cannot be known surfaces as TransactionCommitAmbiguousException naming what each physical source did g07/g05
108 One cross-replica mutual-exclusion primitive, IDistributedLock (TryAcquireAsync(key, ttl, wait)): non-reentrant, TTL-bounded, explicitly best-effort, owner-scoped idempotent release; Redis-backed where a connection exists, a warn-once process-local fallback where not; it collapses duplicate work and is never the only guard on an invariant persistence can enforce g05/g14
109 Feature-by-folder as an enforced convention: the aggregate names the first folder level in Domain/Application/Shared, a technical root then the aggregate in UI/API/Infrastructure, at most twelve direct code files per folder, namespaces follow folders, and a layout change ships as a breaking release with a migration map (FolderWidthTestsBase enforces the cap in every repo) g27
110 Rubric v2 keeps 34 categories with stable numbering, replacing the two overlap-heavy ones in place: section 10 becomes Messaging & Integration Architecture and section 16 becomes AI-Native Application Architecture (N/A until a product feature calls a model), with criteria added to eleven others 99-coverage-audit (rubric matrix; every chapter tag carries the v2 names since the 2026-09-05 retag), g04 (section 10), g19 (section 16)
111 AI session scoring governed as a production dependency: the model call sits behind IAiScoringService (declared in Application, carrying ModelId + PromptVersion, both persisted with every score), a golden-replay + prompt-contract evaluation suite is a deploy precondition, the untrusted half of the prompt is delimited, escaped and redacted, the response is schema-constrained, and token spend is metered and alerted against a budgeted ceiling g18/g19/devops-cicd

The canonical index for the full set can be found at https://ivanball.github.io/docs/adr/.


3. The external stack (BCL / NuGet, "external Level 0")

These are not first-party and get no per-type sections. Versions are from MMCA.Common/Directory.Packages.props and MMCA.ADC/Directory.Packages.props (Central Package Management, see §4). What each is and why it's here:

Web / API

  • ASP.NET Core 10 (minimal hosting, MVC controllers), the API surface.
  • Asp.Versioning.Mvc 10: API versioning for controllers.
  • Microsoft.AspNetCore.Authentication.JwtBearer 10: validates JWT bearer tokens.
  • Yarp.ReverseProxy 2.3.0 (ADC), the gateway that fronts the extracted module services; with Microsoft.Extensions.ServiceDiscovery.Yarp it routes to services by name.

Application / mapping / validation

  • FluentValidation 12: request/command validators, run by a pipeline decorator.
  • Riok.Mapperly 4.3.1: a source-generated, compile-time object mapper (no runtime reflection). Note ADR-001 chose manual DTO mapping over reflection-based AutoMapper; Mapperly is the compile-time, allocation-free way to keep mapping explicit and fast.
  • Scrutor 7: assembly scanning and decorator registration (TryDecorate) for DI; this is how the CQRS decorator pipeline is wired.
  • Microsoft.FeatureManagement 4.6: feature flags (e.g. Notification.PushNotifications).
  • System.Linq.Dynamic.Core: dynamic OrderBy/filtering for query endpoints.

Persistence

  • EF Core 10 with providers SqlServer, Cosmos, and Sqlite: the ORM. Sqlite is used for fast integration tests; Cosmos is a supported document source. EF concepts you must know: DbContext (unit of work + change tracker), entity configurations (IEntityTypeConfiguration<T>), migrations (versioned schema deltas), global query filters (the soft-delete mechanism), and interceptors (SaveChanges hooks for audit + domain-event capture).
  • StackExchange.Redis / SignalR Redis backplane, distributed cache and SignalR scale-out.

Messaging

  • MassTransit 8.5.5 (RabbitMQ + Azure Service Bus transports), the broker abstraction behind IMessageBus's broker implementation. Pinned to v8 by policy: v9 requires a commercial license and crashes broker-enabled hosts at startup; a build-time test fails if the major reaches 9 (MMCA.Common/Directory.Packages.props:49-56 carries the pin and the warning comment, and see §4).

Transport (service extraction)

  • Grpc.AspNetCore / Grpc.Net.ClientFactory / Grpc.Tools / Google.Protobuf: gRPC server + client
    • .proto compilation, for synchronous inter-service calls between extracted modules (ADR-007).

UI

  • MudBlazor 9.7.0: the Blazor component library and design system (grids, dialogs, forms, theme). Used by both MMCA.Common.UI and the ADC UIs.
  • Microsoft.AspNetCore.Components.*: Blazor (Server + WebAssembly) runtime and authorization.
  • Polly 8 (via Microsoft.Extensions.Http.Resilience), retry/timeout/circuit-breaker resilience on outbound HTTP/gRPC clients.

Hosting / observability (.NET Aspire)

  • Aspire.Hosting 13.4.6 (+ RabbitMQ, Azure CosmosDB integrations), local orchestration: the AppHost spins up every service, database, broker, and a dashboard with one command.
  • OpenTelemetry (Api/Exporter/Instrumentation) + Azure.Monitor.OpenTelemetry.AspNetCore, structured logs, distributed traces, and metrics, exported to Azure Application Insights.
  • Microsoft.Extensions.ServiceDiscovery: resolves service names to endpoints (local and cloud).
  • AspNetCore.HealthChecks.*: Redis/RabbitMQ health probes.

Auth / crypto

  • System.IdentityModel.Tokens.Jwt 8: JWT creation/validation; JWKS key publishing for cross-service token validation (ADR-004 "authentication dual-fetch").

Versioning / build

  • MinVer 7: derives the package version from the git tag (vX.Y.Z), so releases are tag-driven.

Analyzers (all at error severity, see §4)

  • Meziantou.Analyzer, SonarAnalyzer.CSharp, StyleCop.Analyzers, Roslynator.Analyzers, Microsoft.VisualStudio.Threading.Analyzers.

Testing

  • xunit.v3 3.2: the test framework (xUnit v3, not v2).
  • Microsoft Testing Platform (MTP): the test runner (global.json sets "runner": "Microsoft.Testing.Platform"), not VSTest. This changes how you run a single test (see §6).
  • bUnit 2: Blazor component testing (the v2 line is the one compatible with xUnit v3 / MTP).
  • Microsoft.Playwright 1.61 + Deque.AxeCore.Playwright 4.12: browser E2E and axe-core accessibility (WCAG 2.1 AA) scanning.
  • NetArchTest.eNhancedEdition: architecture fitness tests (assert layer/purity rules against compiled assemblies).
  • Moq 4 (mocking), AwesomeAssertions 9 (fluent assertions, a FluentAssertions-compatible fork), coverlet (coverage).

4. C#, build, and code-style conventions

  • .NET 10.0, LangVersion: preview: required because the codebase uses C# extension types (extension(T) syntax, see below).
  • Central Package Management (CPM). All NuGet versions live in each repo's Directory.Packages.props (ManagePackageVersionsCentrally = true); individual .csproj files reference packages by name only. To change a version, edit the props file. [Rubric §15, §32]
  • NuGet lock files + pinned, audited sources. MMCA.Common commits lock files and pins packageSourceMapping to nuget.org, so it builds/tests with no GitHub token. CI runs dotnet list package --vulnerable and fails on any vulnerable package. The MassTransit v8 pin is enforced by a build-time test (DependencyVersionTests), a blanket "update all packages" that reintroduces v9 will fail the build by design. [Rubric §32, Dependency & Supply-Chain]
  • TreatWarningsAsErrors globally, and five analyzers at error severity. The code must be warning-free to compile. [Rubric §15, Best Practices & Code Quality]
  • .editorconfig enforces style at error severity (MMCA.Common/.editorconfig): file-scoped namespaces (csharp_style_namespace_declarations = file_scoped:error, line 102), braces always required (csharp_prefer_braces = true:error, line 138), var only when the type is apparent (lines 105-107), expression-bodied members preferred (lines 110-117), all accessibility modifiers required (line 73), no this. qualification (lines 57-60), readonly where possible (line 94), interfaces begin with I (error, line 212). The naming rules below that (private fields _camelCase, constants PascalCase) are declared at warning, which TreatWarningsAsErrors promotes to a build break anyway. Test files relax method-naming and complexity rules via the [Tests/**/*.cs] section (line 737).

C# extension(T) types, read this once

C# 14 (preview) extension members let a static class add members to a type via an extension block:

public static class DomainHelper
{
    extension(string? id)                 // receiver: the "this" value
    {
        public TIdentifier Parse<TIdentifier>() { ... }   // usable as someString.Parse<int>()
    }
}

The codebase uses this heavily for DI registration, every DependencyInjection.cs adds methods like AddApplication() directly onto IServiceCollection through an extension(IServiceCollection) block. You'll first meet the syntax in group-02 (DomainHelper, EntityTypeExtensions). (A practical note for the leveling: references written inside an extension block belong to the enclosing static class, that's how this guide attributes their dependencies.)

Architecture enforcement is doubled (fitness functions) [Rubric §34, §3]

The layer rules are not just convention, they are enforced twice:

  1. Compile-time, Source/Build/MMCA.Common.LayerEnforcement.targets, imported for every MMCA.Common.* project under Source/ (MMCA.Common/Directory.Build.props:99-100), inspects ProjectReferences before build and fails with a descriptive error if a layer references a forbidden upstream layer.
  2. Runtime (test), Tests/Architecture/MMCA.Common.Architecture.Tests (NetArchTest) asserts the same rules against compiled assemblies: layer flow, domain purity, and microservice extraction rules (e.g. Application/Domain/Shared must never reference MassTransit directly, depend on IMessageBus instead). The rule bodies themselves now live once in the shipped MMCA.Common.Testing.Architecture package (one of the lockstep-released packages, see FACTS.md): a reusable rule library + abstract *TestsBase classes that each repo's arch-test project subclasses, supplying only a repo-specific IArchitectureMap, so MMCA.Common and MMCA.ADC (and, outside this guide's scope, MMCA.Store and MMCA.Helpdesk) all enforce one rule set (ADR-015).

When you move a type between packages, expect both gates to react. This is the codebase's "executable governance", covered fully in the architecture-tests chapter.


5. The solution / test layout

  • *.slnx: the human solution (XML format). *.slnf, a solution filter used in CI to build a subset fast (MMCA.Store.CI.slnf, MMCA.ADC.CI.slnf).
  • Microsoft Testing Platform, not VSTest. To run one test class/method you target the test project and pass an MTP filter after --:
    dotnet test --project Tests/<path>/<Name>.Tests.csproj -- --filter-method "*Pattern*"
    #                                                      -- --filter-class  "*FooTests*"
    Always --project <csproj>, never a bare path, and get the flag right: a wrong filter flag exits 5 having run zero tests instead of erroring out loudly. Every test project must contain at least one test or MTP exits 8, so CI passes --minimum-expected-tests on every run: 1 on the ADC legs (MMCA.ADC/.github/workflows/deploy.yml:219) and 2000 on MMCA.Common's full solution run, where the point is to fail a discovery regression that silently drops thousands of tests (MMCA.Common/.github/workflows/ci.yml:144).
  • Some UI test projects (MMCA.Common.UI.Gallery, MMCA.Common.UI.E2E.Tests) are deliberately excluded from the .slnx so the unit-test run stays fast; they run in a dedicated CI job and are built by csproj path.

6. The 34-category architecture-evaluation lens

This codebase is also scored against a 34-category rubric (Website/docs-src/governance/ArchitectureEvaluationCriteria.md, published at https://ivanball.github.io/docs/governance/). This guide weaves the rubric in so you learn the system and the lens it's judged by at the same time. Each type section tags the categories it genuinely touches as [Rubric §N, Name], with a one-line "what §N assesses" and "how this code embodies (or under-uses) it". The first occurrence of a category teaches it; later ones cross-reference back. The guide explains categories; it does not score them, the filled scorecards live beside the rubric as one repo-prefixed file per repo (Website/docs-src/governance/common-ArchitectureScorecard.md, adc-ArchitectureScorecard.md, store-ArchitectureScorecard.md), each paired with a *-RemediationBacklog.md.

Two axes (so a tag can say "mature but mediocre" or vice-versa)

  • Maturity (0 to 4), process: how consistently/automatically the pattern is governed (ad-hoc → enforced by CI).
  • Implementation (0 to 10), substance: how good the implementation is right now, against the category's criteria and red flags.

The categories, in three parts (quick index, full criteria in the rubric file)

Part A, Application / Backend (§1 to §17): §1 SOLID · §2 Design Patterns · §3 Clean Architecture · §4 Domain-Driven Design · §5 Vertical Slice · §6 CQRS & Event-Driven · §7 Microservices Readiness · §8 Data Architecture · §9 API & Contract Design · §10 Messaging & Integration Architecture · §11 Security · §12 Performance & Scalability · §13 Observability & Operability · §14 Testability & Test Strategy · §15 Best Practices & Code Quality · §16 AI-Native Application Architecture · §17 DevOps & Deployment.

Part B, Front-End / UI (§18 to §28): §18 UI Architecture & Component Design · §19 State Management & Data Flow · §20 Design System, Theming & UI Consistency · §21 Accessibility (a11y) · §22 Responsive Design & Cross-Browser/Device · §23 Front-End Performance & Rendering · §24 Forms, Validation & UX Safety · §25 Navigation, Routing & Information Architecture · §26 Front-End Security · §27 Internationalization & Localization · §28 Front-End Testing & Quality.

Part C, Operational, Governance & Cross-Cutting (§29 to §34): §29 Resilience, Reliability & Business Continuity · §30 Compliance, Privacy & Data Governance · §31 Cost Efficiency / FinOps · §32 Dependency & Supply-Chain Management · §33 Developer Experience & Inner Loop · §34 Architecture Governance & Documentation.

Some categories live most naturally in the DevOps/test chapters (§13 to §14, §17, §28, §29 to §34) and are explained there. The coverage audit will include a matrix proving every one of the 34 is explained at least once against real code or a real artifact.


You're ready for group-01, Result & Error Handling.