Onboarding guide
29. Testing & Quality Infrastructure
What this group covers. Everything the codebase uses to prove itself: the six reusable
test-support packages that ship out of MMCA.Common/Source/Hosting (MMCA.Common.Testing,
MMCA.Common.Testing.Architecture, MMCA.Common.Testing.Aspire, MMCA.Common.Testing.Azure,
MMCA.Common.Testing.E2E, MMCA.Common.Testing.UI, six of the twenty-six packages published from
MMCA.Common and listed in MMCA.Common/FACTS.md:19-47; the seventh test-support package,
MMCA.Common.AI.Testing, belongs to group-27), the
architecture-fitness rule library that gates the build, the runtime-conformance bases that gate a
booted host, the backend-less component Gallery harness, the BenchmarkDotNet performance suite, the
weekly load tier, and
the many per-repo test projects that consume all of it. The distinction to hold onto while reading:
most of the types in this group are reusable bases, fixtures, harnesses, and helpers compiled
into and shipped by MMCA.Common, while the concrete [Fact]-bearing test classes that subclass them
live in each consumer repo (MMCA.Common.*.Tests, MMCA.ADC.*.Tests, MMCA.Store.*.Tests). Those
individual test classes are cataloged by project in the companion rollup section; this chapter
teaches the machinery they stand on. For how the tiers map onto CI jobs and solution filters, see
Testing Architecture & Solution Composition.
There are seven moving parts. Five map onto the test pyramid, one is a governance layer, and one evaluates a dependency that is not deterministic and so cannot be asserted exactly:
- Integration-test scaffolding (
IIntegrationTestFixture,IntegrationTestBase<TFixture>,SqlServerIntegrationTestFixtureBase<TEntryPoint>,CrossServiceFixtureBase,CrossServiceHostIsolation,ProductionHostApplicationFactory<TEntryPoint>,JwtTokenGenerator,FeatureManagementTestExtensions,TestPolling,EntityBuilderBase<TBuilder, TEntity>) boots a real service host in-process against a throwaway SQL Server database and drives it over HTTP. - Architecture fitness functions (
IArchitectureMap,ArchitectureMapBase,Layer,LayerRef,ArchitectureAssert,RuleHelpers,CrossEntityNavigationFinder, the forty-fourArchitectureRulespartial files, and the sixty-four abstract*TestsBaseclasses includingRouteAuthorizationTestsBase,ModuleConformanceTestsBase<TModule>,SubstitutabilityTestsBase,PiiInventoryTestsBaseandBrandColorTokenTestsBase) turn architectural rules into build-gating assertions that run identically across every repo. - Component (bUnit) testing (
BunitComponentTestBase,TestPrincipal,BunitInteractionExtensions,CapturingHttpMessageHandler,UiHttpServiceHarness,HttpTestDoubles,StubTokenStorageService,MarkupSnapshot,FieldErrorAccessibilityExtensions) render Blazor components in isolation with real MudBlazor services and faked HTTP/auth edges; the last of them asserts that a field's validation error reaches assistive technology, not just sighted users (arole="alert"element,aria-invalid="true"on the field, and anaria-describedbytarget holding the message,MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:7-26). - End-to-end (Playwright) testing (
PlaywrightFixture,E2ETestBase,E2ETestConfiguration,AuthOutcomeRules,PageExtensions,AxeOptions,AccessibilityViolationException,WebVitalsCollector, the reusable page objectsLoginPage/RegisterPage/ProfilePage/ForgotPasswordPage/ResetPasswordPage/RoleAdminPage, theE2EPollingwait helper, and the shipped workflow suites such asAuthorizationTestsBaseandKeyboardOperabilityTestsBase, whose four keyboard-only checks cover the skip link, the dialog focus trap, Escape returning focus to the opener, and a visible focus indicator on the leading Tab stops,MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:11-29,41) drive a real browser against a running app, asserting accessibility and performance alongside behavior. - Contract and pipeline bases (
SecurityHeadersTestsBase,OpenApiContractTestsBase<TFixture>,OpenApiBreakingChangeDetector,TenantResolutionTestsBase,ProblemDetailsContractTestsBase<TFixture>,ServiceInfoVersioningContractTestsBase<TFixture>,GracefulShutdownTestsBase<TEntryPoint>,DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>,MiddlewarePipelineOrderTestsBase,DependencyInjectionAssert,HandlerTestBase<THandler>) pin cross-cutting HTTP and pipeline guarantees so a refactor cannot silently drop them. - Model-evaluation tests (
GoldenCase,GoldenCorpus,GoldenReplayTests,LiveJudgeTests,PromptContractTests,SessionScoringPromptContractPinTests) hold ADC's AI session-scoring judge to a committed corpus of recorded cases, one half replayed offline on every CI leg and one half scored by the real model behind a trait gate. - Orchestration smoke tests (
AppHostFixtureBase,AppHostTestBase<TFixture>,AppHostEnvironmentRequirement,AppHostEnvironmentGate,DockerAvailability,DeveloperCertificateAvailability,EphemeralRsaKeyPair,AppHostReadinessBudget,AppHostProbePaths,H2cProbe) boot a real Aspire AppHost once per collection and assert the orchestration every other tier boots past.
The cloud adapter packages of chapter 28 add a vendor
dimension to that pyramid, and the group carries it in three layers. Each adapter package has a plain
unit-test project (MMCA.Common.Aws.Tests, MMCA.Common.Azure.Tests, MMCA.Common.Gcp.Tests) that
drives the adapter against in-process fakes such as FakeSecretsStore,
FakeFcmMessagingClient and
InMemoryGcsStorageClient, and each carries a
CloudVendorRulesTests class of its own. Behind them sit the emulator
tiers, each starting one pinned container per test assembly through an xUnit assembly fixture:
LocalStackFixture for S3, SQS, SNS, Secrets Manager, Systems Manager and
DynamoDB (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.LocalStack.Tests/LocalStackFixture.cs:9,19),
and FakeGcsServerFixture plus
FirestoreEmulatorFixture for Cloud Storage and the FCM installation
registry (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FakeGcsServerFixture.cs:7,18,
FirestoreEmulatorFixture.cs:5,14). Each image is pinned so an upstream emulator change cannot turn
into a red CI run nobody caused (LocalStackFixture.cs:14-17). The Azure Service Bus tier is the
one that ships as a package: ServiceBusEmulatorFixtureBase
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Azure/Fixtures/ServiceBusEmulatorFixtureBase.cs:54)
starts one warm emulator for the whole broker-parity tier to respect the emulator's connection quota
and admin-plane throttle (:12-18), pins a 2.x image because the management plane MassTransit
provisions its topology through first shipped in 2.0.0 (:20-27), keeps the bus on the fixture
rather than on the per-test class so the topology is provisioned once per run (:29-38), and bounds
both startup phases so a hang fails with a named timeout instead of being killed silently at the job
timeout (:40-46). The third layer is the fitness rule that keeps the vendor SDKs out of the core:
CloudVendorIsolationTestsBase
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:15)
fails the build when an assembly a repo declares cloud-neutral names an Azure, AWS or Google Cloud SDK
type, references a vendor SDK or broker-transport assembly, or references a vendor adapter package
(:4-7, two facts at :27-33). It is the compiled-assembly half of the EnforceCloudVendorBoundary
build guard, which judges only the declared package references, so a vendor type arriving through a
transitive path is caught too (:9-12,
ADR-134).
MMCA.Common activates it in CloudVendorIsolationTests
(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17).
This whole group is the [Rubric §14, Testability] story made concrete: the framework does not merely
permit testing, it ships the reusable substrate so every consumer tests the same way. The
front-end tiers additionally carry [Rubric §21, Accessibility], [Rubric §22,
Responsive/Cross-Browser], [Rubric §23, Front-End Performance], and [Rubric §28, Front-End Testing];
the fitness library carries [Rubric §34, Architecture Governance & Documentation]. Two ADRs govern
the two governance tiers and are worth reading before touching anything here:
ADR-015 for the
structural rules, and
ADR-058 for
the runtime conformance suites that cover exactly what ADR-015 declared out of scope: "the tests
assert structure / registration, not runtime behavior"
(Website/docs-src/adr/015-architecture-fitness-functions.md:64-65).
Integration tests: a real host, a throwaway database, a per-test reset
The integration tier boots the actual application, not a mock of it. The abstraction at its center
is IIntegrationTestFixture
(MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8): a two-method contract, CreateClient()
(IIntegrationTestFixture.cs:11) and ResetDatabaseAsync() (IIntegrationTestFixture.cs:19), that
hides how the host and its database are provisioned. Its remarks are load-bearing: a host running
multiple physical data sources (database per service, see
primer and
ADR-006) must reset every
relational source, and a fixture can resolve IEntityDataSourceRegistry / IDataSourceResolver
from the booted host to enumerate them (IIntegrationTestFixture.cs:13-18).
IntegrationTestBase<TFixture>
(MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13) is the per-test base every integration test class
inherits. It implements xUnit's IAsyncLifetime, so InitializeAsync resets the database before
each test (IntegrationTestBase.cs:31) and DisposeAsync disposes the HTTP client after
(IntegrationTestBase.cs:34-39). It exposes typed HTTP helpers (GetAsync<T>, PostAsync<T>,
PutAsync<T>, PutAsync, DeleteAsync, IntegrationTestBase.cs:51-72), bearer-token management
(SetBearerToken / ClearAuthentication, IntegrationTestBase.cs:42-48), and a thread-safe
NextId() counter seeded at 1000 (IntegrationTestBase.cs:16,75) so parallel tests never collide
on generated identifiers. Downstream projects subclass it to add domain-specific auth and entity
helpers.
SqlServerIntegrationTestFixtureBase<TEntryPoint>
(MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) is the concrete fixture
scaffolding. InitializeAsync (SqlServerIntegrationTestFixtureBase.cs:67) mints a GUID-suffixed
database name (:71-72), sets ASPNETCORE_ENVIRONMENT=Testing and the top-level connection string
as process environment variables (so the host reads them at configure-time, :75-77), builds the
subclass-supplied WebApplicationFactory (:79), and forces database creation by requesting the
first client, which runs the host's Migrate init strategy (:87-89). The "may need dropping" flag is
set before that first client (:81-85), and the drop is guarded by IF DB_ID(...) (:186-188), so a
host that fails after its migration created the database still has it dropped rather than leaked on
the shared server. It then builds a Respawn checkpoint that ignores __EFMigrationsHistory
(:95-99); ResetDatabaseAsync (:104) replays that checkpoint between tests, and DisposeAsync
(:120) drops the throwaway database (:130-133, DropDatabaseAsync at :172) and restores every
pushed environment variable (:135, restore loop at :162-170, first-value-wins bookkeeping at
:151-160 so a re-pushed key cannot clobber its own restore point). The Testing
environment is chosen deliberately so appsettings.Development.json (which points a module's
DataSources entry at localhost) does not load, leaving the resolver to collapse onto the
overridden top-level connection string, a single-database monolith shape (:16-24). Server selection
defaults to LocalDB but is overridable through SqlBaseEnvironmentVariable (:58, read at :69-70)
so CI can target a SQL service container. Subclasses push their own host-specific settings (test JWT
key material, throttle lifts, faked gRPC edges) through the ConfigureTestEnvironment hook (:147,
invoked at :77), which routes them through the same restore bookkeeping. The fixture also exposes
ConnectionString (:45) so SQL-fidelity tests can read the raw tables, and Services (:52) so a
cross-service test can resolve a consumer-side handler out of the booted host. Because these fixtures
need a reachable SQL Server, the per-module *.Integration.slnf suites build in a headless sandbox
but only run in CI.
One tier up sits CrossServiceFixtureBase
(MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:47), which boots several hosts in one process
against a real Testcontainers SQL Server and a real Testcontainers RabbitMQ
(CrossServiceFixtureBase.cs:2-3,18-25), so the genuine outbox to broker to consumer round-trip is
exercised rather than faked. Each logical source it routes is a
CrossServiceDataSource record pairing the config key with its physical
database (CrossServiceFixtureBase.cs:15, list supplied by the subclass at :60). The design note
worth internalizing is why the hosts must boot strictly sequentially: every host reads its
connection string, MessageBus settings and JWT settings from configuration at configure-time,
before builder.Build(), so process environment variables are the only override channel that lands
in time, and the one genuinely per-host key is the SQL connection string
(CrossServiceFixtureBase.cs:26-39). A booted host has already snapshotted its connection, so
mutating the environment for the next one is safe. Two smaller decisions in the same file are worth
knowing because they are non-obvious: the databases are pre-created before any host boots, since
EF's CREATE DATABASE runs before its migration lock is taken and a double-booted host would race
itself (CrossServiceFixtureBase.cs:107-111), and each module is routed to a named data source
whose connection string differs only by Application Name, so EF's process-global model cache keys
each host's model separately instead of letting the first booted host win
(CrossServiceFixtureBase.cs:231-246,260-276). Booting several hosts in one process has a second
trap that the model-cache keying does not close: the default entity-configuration assembly provider
scans AppDomain.GetAssemblies, so each host would see its peers' module assemblies, claim their data
sources, and let its outbox and internal-command processors drain the peer databases, dead-lettering
a row as handler_missing when only the peer holds its handler
(MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:14-21).
CrossServiceHostIsolation (CrossServiceHostIsolation.cs:30) closes it
with one extension member, IsolateCrossServiceHost<TEntryPoint>() (:42), which every host factory
of a CrossServiceFixtureBase subclass calls from ConfigureTestServices: it replaces the registered
provider with a private decorator, HostScopedAssemblyProvider
(:115-118), that narrows the inner result to the assemblies the host's own .deps.json names (the
set it would load in its own process) and keeps explicitly registered AdditionalAssemblies unfiltered
(:23-28,47-59), and it throws when that .deps.json is missing rather than silently scanning
everything (:86-91). ADC's four cross-service factories each make that call
(MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:46,
and the Identity, Engagement and Notification siblings at IdentityCrossServiceFactory.cs:40,
EngagementCrossServiceFactory.cs:55, NotificationCrossServiceFactory.cs:56). A host that needs no database at all gets the much
smaller boot path:
ProductionHostApplicationFactory<TEntryPoint>
(MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) pins UseEnvironment("Production")
(ProductionHostApplicationFactory.cs:36) so the production-only branches (restrictive CORS, HSTS
emission) are the ones under test, and captures the started IHost
(ProductionHostApplicationFactory.cs:29) because StopAsync is not reachable through the
WebApplicationFactory surface at all.
Four helpers round out the tier. JwtTokenGenerator
(MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30) issues RS256-signed tokens (GenerateToken,
JwtTokenGenerator.cs:112, signing credentials built at :130-131) using an embedded development
RSA keypair (DefaultPublicKeyPem at :49, DefaultPrivateKeyPem at :68) under a fixed kid of
mmca-test-key (:41), so integration tests exercise the exact JWKS/RS256 validation code path
production runs (ADR-004);
its ConfigureInProcessTokenValidation (:170) is what a test factory calls to re-point a host's
JwtBearerOptions at that committed key instead of a network authority (it nulls both Authority
and ConfigurationManager, :174-175). The class remarks flag, correctly, that the committed
keypair is insecure by design and must never be used in a real deployment (:22-28).
FeatureManagementTestExtensions
(MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10) adds a ConfigureTestFeatureFlags
extension member (:35) that layers an in-memory FeatureManagement:* collection on top of the
configuration the host already registered and re-registers the merged root in its place (:40-59);
building a flags-only root instead would silently hand every component constructed afterwards a
configuration with nothing but feature flags in it, because .NET DI resolves a non-collection
dependency to the last registration (:18-25). TestPolling
(MMCA.Common.Testing/Support/TestPolling.cs:9) replaces the pre-assert sleep that eventually-consistent
paths tempt you into: PollUntilAsync (TestPolling.cs:22) probes until the condition holds or a
60-second budget expires at a 500 ms interval (TestPolling.cs:31-32) and returns the last probed
value either way, so a timeout still fails on the real assertion message.
EntityBuilderBase<TBuilder, TEntity>
(MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9) is a minimal fluent-builder base whose single
abstract Build() (:17) returns the entity through its domain factory, so test setup specifies
only what a test cares about. Together these embody [Rubric §11, Security] (real token validation
rather than bypassed auth middleware) and [Rubric §14, Testability].
Orchestration smoke tests: booting a real AppHost
Every tier above boots hosts directly, which means none of them ever runs the file that states how a
whole stack fits together. The AppHost declares the project resources, the per-service databases, the
broker, the JWKS and gRPC references and the WaitFor graph, and nothing else compiles a claim about
any of it: a build type-checks its C# but never executes it, and the integration tier boots each host
through WebApplicationFactory, bypassing the orchestration entirely
(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Aspire/Fixtures/AppHostFixtureBase.cs:16-20,
Website/docs-src/adr/117-apphost-integration-test-base.md:13-21). MMCA.Common.Testing.Aspire is
the tier that closes that gap, shipped as one of the five test-support packages rather than copied
per repo
(ADR-117, accepted
2026-09-09 and revised 2026-09-11 once both consumer tiers subclassed it,
Website/docs-src/adr/117-apphost-integration-test-base.md:3-10), and it adds the tier
ADR-098
deliberately left out.
AppHostFixtureBase
(MMCA.Common.Testing.Aspire/Fixtures/AppHostFixtureBase.cs:41) is the xUnit collection fixture: it
boots a real AppHost once and hands the running DistributedApplication to every test in the
collection. Its central distinction is readiness rather than liveness, per resource. "The application
started" only means the orchestrator launched the processes, so resources carrying a health check are
awaited healthy while resources carrying none can only be awaited Running, and the fixture
says which of the two happened instead of pretending they are the same
(AppHostFixtureBase.cs:22-28). Like the SQL fixtures it records only the first original value of
every environment variable it pushes, so a re-pushed key cannot clobber its own restore point and a
collection cannot leak key material into the ones that run after it (AppHostFixtureBase.cs:43-48).
An unmet precondition is a skip, not a failure: the gate sets SkipReason and nothing is started
(AppHostFixtureBase.cs:56, IsAvailable at :59), and the consuming project owns the skip call,
because this package deliberately takes no dependency on the xUnit assertion library
(AppHostFixtureBase.cs:29-34). The call is written as a branch,
if (!Fixture.IsAvailable) { Assert.Skip(Fixture.SkipReason!); }, never as
Assert.SkipWhen(!Fixture.IsAvailable, Fixture.SkipReason!): SkipWhen validates its reason before
the condition, and the reason is null exactly when the stack did start, so that form throws on every
runner able to boot the AppHost (AppHostFixtureBase.cs:34-38).
What counts as a precondition is declared, not guessed.
AppHostEnvironmentRequirement
(MMCA.Common.Testing.Aspire/Preconditions/AppHostEnvironmentRequirement.cs:10) is a [Flags] enum
of None, OptIn, Docker and DeveloperCertificate (:12-30), and
AppHostEnvironmentGate
(MMCA.Common.Testing.Aspire/Preconditions/AppHostEnvironmentGate.cs:9) turns the missing ones into
a sentence a human can act on, or null to run (AppHostEnvironmentGate.cs:23-27, with a testable
core that takes every machine fact as a delegate at :38-42). The opt-in is the
MMCA_APPHOST_TESTS variable (AppHostEnvironmentGate.cs:16), absent by default so an ordinary unit
loop never starts an orchestrator (:12-14). The other two probes encode failures that cost real
debugging time. DockerAvailability
(MMCA.Common.Testing.Aspire/Preconditions/DockerAvailability.cs:13) checks DOCKER_HOST, the
/var/run/docker.sock socket and the docker_engine named pipe (DockerAvailability.cs:16,19,22)
rather than shelling out to docker info, because a process launch that can block for seconds on a
misconfigured daemon is the wrong thing to put in front of a skip decision
(DockerAvailability.cs:6-11).
DeveloperCertificateAvailability
(MMCA.Common.Testing.Aspire/Preconditions/DeveloperCertificateAvailability.cs:22) detects the
ASP.NET Core HTTPS development certificate by the SDK's own extension OID
1.3.6.1.4.1.311.84.1.1 (:28, validity judged at :44-52) and detects only, never running
dotnet dev-certs (:15-20): without that certificate an https resource answers its probe with
UntrustedRoot, never turns healthy, and every WaitFor edge into it waits out the whole budget, so
the symptom is a timeout on an unrelated resource (:7-14).
EphemeralRsaKeyPair
(MMCA.Common.Testing.Aspire/Preconditions/EphemeralRsaKeyPair.cs:20) closes the matching auth-side
failure: it mints a throwaway RSA-2048 keypair (:38,42-46) into E2E_JWT_PRIVATE_KEY_PEM and
E2E_JWT_PUBLIC_KEY_PEM (:26,32), the variables the framework's WithE2eRsaKeys() AppHost
extension forwards to an Identity resource, because otherwise the appsettings.json placeholder
reaches the JwtBearer options factory, which throws on the first request, so every request including
the liveness probe answers 500 and the resource never turns healthy (:8-16).
Timing and probing are the remaining pieces. AppHostReadinessBudget
(MMCA.Common.Testing.Aspire/Fixtures/AppHostReadinessBudget.cs:16) splits the wait into a startup
budget and a readiness budget, five minutes each by default (:23-24), because the two fail for
different reasons (a cold agent pulling container images versus a wedged health check) and one number
makes those indistinguishable in the failure message (:6-11); Remaining(elapsed) (:37-38) keeps
readiness a single shared deadline rather than a per-resource allowance, so a stack that wedges on the
last of five resources still says so once (:28-33), and Validate (:45-49) rejects a
non-positive budget that would cancel its own first wait.
AppHostProbePaths
(MMCA.Common.Testing.Aspire/Probes/AppHostProbePaths.cs:15) pins the paths a probe uses, /health
(:18), /alive (:26), /health/ready (:32), the key set at /.well-known/jwks.json (:35)
and the cleartext endpoint name http (:41); it mirrors the framework constants instead of
referencing them, with a unit test in MMCA.Common.Testing.Aspire.Tests cross-asserting each pair so
a rename cannot silently orphan a probe (:5-13). Its /alive remark carries the operational
lesson: gate a startup wait on liveness, never on readiness, because a readiness endpoint aggregates
downstream and warm-up checks and a gateway waiting for a service whose own readiness warms up
through that gateway deadlocks (:20-25). H2cProbe
(MMCA.Common.Testing.Aspire/Probes/H2cProbe.cs:25) asserts that a service's cleartext listener
really speaks the protocol its gRPC callers need: with no TLS there is no ALPN to negotiate with, so
the request goes out as HTTP/2 with prior knowledge pinned by RequestVersionExact and an
HTTP/1.1-only listener answers with a connection error rather than a response (:9-16), under a
deliberately short ten-second budget (:28); its keep-alive ping trio (one-second delay, one-second
timeout, Always, :30-36) is load-bearing rather than tuning, because HTTP/2 pools one connection
per origin and a connection accepted but never answered would otherwise queue every later probe
behind it forever (:17-23). On top of all of it,
AppHostTestBase<TFixture>
(MMCA.Common.Testing.Aspire/Fixtures/AppHostTestBase.cs:29) gives a test the two things only a
running application can supply, an HttpClient bound to a named resource endpoint (:44-45) and a
resolved connection string (:54-55), plus an RS256 bearer token the booted stack accepts (:57-58).
The package tests itself both ways: unit tests over the gates, budgets and probes in
MMCA.Common.Testing.Aspire.Tests, and a real boot in MMCA.Common.Testing.Aspire.AppHostTests,
where SampleAppHostFixture
(MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Aspire.AppHostTests/SampleAppHostFixture.cs:13),
SampleAppHostCollection (SampleAppHostCollection.cs:9) and
SampleAppHostTests (SampleAppHostTests.cs:21) exercise the fixture against
a minimal AppHost. This tier is [Rubric §14, Testability] pointed at the one file every other tier
looks past.
Architecture fitness functions: rules that gate the build
The layering and DDD conventions this codebase commits to are not left to code review, they are
executed as tests. The reusable rule library lives in MMCA.Common.Testing.Architecture and is the
subject of ADR-015.
Its keystone is IArchitectureMap
(MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39): the single per-repo boundary every
fitness function keys off. Each repo supplies one implementation (for example
StoreArchitectureMap) declaring its layer and module assemblies as LayerRef records
(IArchitectureMap.cs:31) tagged by the Layer enum (IArchitectureMap.cs:9), and
exposes them through query members such as OfLayer, ModuleDomain, ModuleApplication, For,
ModuleOf, and OtherModuleNamespaces (IArchitectureMap.cs:51-81). Most of that surface is
derived rather than hand-written: ArchitectureMapBase
(MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11) computes the projections from a single
DefineLayers() declaration (ArchitectureMapBase.cs:22, lazily materialized at :13,15-16,25),
which also centralizes every assembly and namespace string in one file so Ubuntu CI's case
sensitivity is handled in one place (ArchitectureMapBase.cs:8-9); it additionally ships a
FindRepoRoot(solutionFileName) walker (ArchitectureMapBase.cs:79) so doc- and
config-consistency rules can read committed files no matter what the runner's working directory is.
The shared rules consume only the interface, which is why one rule body runs identically across
MMCA.Common, MMCA.Store, MMCA.ADC, and Helpdesk: the map is the only thing that varies. Layer
deliberately includes optional layers (Ui, Grpc, Contracts, ServiceHost,
IArchitectureMap.cs:16-19) that a repo simply omits, so a rule iterating them is vacuously
satisfied with no compile dependency on an absent assembly (IArchitectureMap.cs:3-7).
The rule bodies are split across forty-four ArchitectureRules partial files
(AI model boundary, anonymization, cancellation tokens, cascade soft-delete, clock reads, cloud vendors, command validators, contracts,
controllers, cycles, delete behavior, domain-event handler saves, domain throws, entities, error
catalog, events, feature flags, folder width, forwarded JWT audiences, governance, handlers, handler
results, idempotency, immutability, inline styles, integration-event purity, layers, lifetime tokens,
localization, localized text, markup, modules,
naming, PII inventory, protos, purity, query-handler repositories, slices, soft delete, specifications, strongly typed ids,
substitutability, transport, and
upcasters; the partial
type is declared in the first of them at
MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:5). The
aggregate-convention rules live inside ArchitectureRules.Entities.cs (for example
DomainExposesAggregateRoots at MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:8,
AggregateRootsHaveResultFactory at :19, and the generalized DomainFactoriesReturnResult at
:53 that extends the "factories always return Result<T>" convention from aggregate roots to
value objects) rather than in a dedicated partial, and are surfaced through
AggregateConventionTestsBase
(MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10). Above the rules sit
the abstract *TestsBase classes under Bases/, sixty-four today (MMCA.Common/FACTS.md:89:
LayerDependencyTestsBase, DomainPurityTestsBase, MicroserviceExtractionTestsBase,
ModuleIsolationTestsBase, PiiConventionTestsBase,
DependencyVersionTestsBase,
IntegrationEventContractTestsBase, DataResidencyTestsBase, RawQueryableConventionTestsBase,
and more), each exposing its rules as [Fact]s that a sealed per-repo subclass activates by
supplying its map. Five of the newest carry the pattern into areas that used to be review questions:
AiDependencyIsolationTestsBase
(MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19) confines a
language-model dependency to Infrastructure and the governed MMCA.Common.AI package in both
directions, so a vendor SDK reference and a PromptContract used as a domain type both fail
(AiDependencyIsolationTestsBase.cs:3-13, [Rubric §16, AI-Native]);
DeleteBehaviorConventionTestsBase
(Bases/Domain/DeleteBehaviorConventionTestsBase.cs:21) reads the MMCA:DeleteBehaviorSource stamp
the framework's RestrictDeleteByDefaultConvention leaves on every foreign key back off a finalized
EF Core model, so nothing cascades unless a configuration said so, asserted against an in-memory
relational model with no server needed (DeleteBehaviorConventionTestsBase.cs:3-19);
FeatureFlagLifecycleTestsBase
(Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10) makes every flag constant declare whether it
is a permanent capability switch or a temporary rollout toggle and fails the build once a temporary
one is past its RemoveBy date, with the judging date overridable so a release, not the build clock,
decides (FeatureFlagLifecycleTestsBase.cs:3-9,14-18, ADR-031);
IntegrationEventPayloadPurityTestsBase
(Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:33) requires an integration event to
ship from a *.Shared assembly and no property on its wire shape to reach a *.Domain type, nested
payload records included
(IntegrationEventPayloadPurityTestsBase.cs:3-26,
ADR-010); and
RawSqlConventionTestsBase
(Bases/Cqrs/RawSqlConventionTestsBase.cs:31) bans FromSqlRaw, SqlQueryRaw, ExecuteSqlRaw and
ExecuteSqlRawAsync in module code while leaving the interpolated siblings and the framework's
IRawSqlQueryExecutor allowed, because the raw four take a plain string, so a concatenated value
compiles and every distinct value produces its own statement text
(RawSqlConventionTestsBase.cs:5-26). A sixth,
StronglyTypedIdTestsBase
(Bases/Domain/StronglyTypedIdTestsBase.cs:9), is vacuously satisfied in every repo today: the
primitive identifier aliases remain the default identifier model, and the base exists so the first
wrapper a repo declares is held to the shape the framework's converters, comparer and route binding
are written against (StronglyTypedIdTestsBase.cs:3-8, ADR-115). Two more pin a testability
property and a read/write boundary:
ClockReadTestsBase
(Bases/Domain/ClockReadTestsBase.cs:15) fails when Domain or Application code reads
DateTime.UtcNow, DateTime.Now or their DateTimeOffset siblings, so a handler injects
TimeProvider and passes the instant in, with BaseDomainEvent's occurrence stamp always exempt and
a reviewed AllowedClockReaders list for the deliberate cases (ClockReadTestsBase.cs:3-12,23, the
rule at Rules/Domain/ArchitectureRules.ClockReads.cs:55); and
QueryHandlerReadRepositoryTestsBase
(Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15) reads the IL of every IQueryHandler<,>,
async and lambda bodies included, and fails any that calls IUnitOfWork.GetRepository instead of
GetReadRepository, because the write repository hands a query a mutation surface it must not use
(QueryHandlerReadRepositoryTestsBase.cs:3-13, the rule at
Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:37). ADC activates both with one-line
sealed subclasses (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8,
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9).
AggregateConventionTestsBase shows the shape in miniature: one abstract Map
property and one [Fact] per rule
(MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:12-24). How many fitness
methods the package ships, and how many of them MMCA.Common's own build executes, is a generated and
CI-gated number in MMCA.Common/FACTS.md:80-85: read it there rather than restating it anywhere
else.
Failures report through ArchitectureAssert
(MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8), which has two overloads: one lists the
failing types from a NetArchTest TestResult (ArchitectureAssert.cs:11-23), the other lists a
reflection-derived violation set (ArchitectureAssert.cs:26-32). Rules NetArchTest cannot express
(method return types, generic constraints, property accessors, attribute usage) reflect over loaded
types via the internal RuleHelpers
(MMCA.Common.Testing.Architecture/RuleHelpers.cs:14), whose LoadableTypes extension property
tolerates a partially resolvable assembly by falling back to the ReflectionTypeLoadException's
resolved types (RuleHelpers.cs:19-33), and whose HasPublicMutableSetter treats an init-only
setter as immutable by checking for the IsExternalInit required modifier
(RuleHelpers.cs:121-137). One such walk,
CrossEntityNavigationFinder
(MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), is an
ExpressionVisitor that collects the entity types a specification's criteria navigates to beyond
its own (ArchitectureRules.Specifications.cs:101-119), because in a polyglot setup that navigation
may cross a physical data source where the join is not translatable
(ArchitectureRules.Specifications.cs:9-15). These runtime rules are the second of two enforcement
layers, the first being the compile-time MSBuild layer guard
(MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets, see
group 14);
ADR-015 describes
both, and this is the clearest [Rubric §34, Architecture Governance] expression in the codebase.
The fitness library reaches beyond pure layering into cross-cutting product guarantees.
RouteAuthorizationTestsBase
(MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22, tagged rubric §25 in its
own remarks, [Rubric §25, Navigation & IA]) reflects over routable Blazor pages and asserts every
governed page keeps its [Authorize(Roles = "...")] gate
(RouteAuthorizationTestsBase.cs:49-61), so an admin route cannot regress to a bare [Authorize]
any authenticated user can reach. It detects RouteAttribute and AuthorizeAttribute by full-name
reflection (RouteAuthorizationTestsBase.cs:24-25) so the package stays free of ASP.NET references,
and a MinimumGovernedPages floor (:47, asserted at :63-74) guards against a moved namespace
silently emptying the scan. ModuleConformanceTestsBase<TModule>
(MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21) pins the three-member
contract ModuleLoader actually registers on (Name, Dependencies, RequiresDependencies),
because drift in any of them does not throw, it silently reorders registration or swaps a real
service for a disabled stub (ModuleConformanceTestsBase.cs:3-10); it reads those members through
the IModule full name by reflection (:24) so the package stays free of the framework's
transitive graph and still dispatches to the interface's default implementations (:12-17).
BrandColorTokenTestsBase
(MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13, [Rubric §20, Design System
& Theming]) reads landing-page stylesheets embedded as manifest resources and fails the build if a
host re-hardcodes the brand hex #1565C0 instead of sourcing var(--mmca-primary) from the shared
token (BrandColorTokenTestsBase.cs:15-16,41-49), with a non-empty check on the embedded list so the
guard cannot pass vacuously (:27-28). DependencyVersionTestsBase
(MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:16, [Rubric §32, Dependency
& Supply-Chain]) checks the repo's pinned package majors and fails the build on the MassTransit
commercial-license trap a blanket package bump would otherwise walk into unnoticed: any of the five
MassTransit packages it lists (DependencyVersionTestsBase.cs:18-25) at major 9 or above (:27-40,
ADR-016), a
failure CI would never see on its own because no CI leg starts a broker (:4-6). A consumer that pins
only some of those packages overrides the list rather than inheriting it (:7-14).
ConstructorDependencyCountTestsBase
(Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:20) turns the SRP judgement call into three
numeric ceilings, one each for Application *Service facades, API controllers and CQRS handlers
(MaxConstructorDependencies, MaxControllerConstructorDependencies,
MaxHandlerConstructorDependencies at :34,41,49), each a ratchet a consumer sets at its current
high-water mark rather than a budget (:14-16, [Rubric §1, SOLID]), with its own non-vacuity guard
so a scan that finds no services fails rather than passes
(Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:60-61).
ConstructorDependencyCountTestsBaseTests
(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:14)
proves the two newer populations actually fail on drift and name the offender, driving private
subclasses over fixture controllers and handlers such as
FatFixtureController so xUnit does not collect their deliberately failing
facts (ConstructorDependencyCountTestsBaseTests.cs:6-13), and
ObservabilityConventionTestsBase
(Bases/Governance/ObservabilityConventionTestsBase.cs:30) pairs every SLO alert a consumer's
infra/main.bicep provisions with a same-severity triage section in its infra/OPERATIONS.md, in
both directions, with a minimum-spec floor so a drifted parse anchor fails loudly instead of passing
with zero discovered alerts (Bases/Governance/ObservabilityConventionTestsBase.cs:6-13,39, [Rubric
§13, Observability & Operability]). MMCA.Common runs that base against the artifact it ships rather
than against a fixture: SampleDeploymentObservabilityTests
(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12)
points it at samples/deployment, the worked example consumers copy, pairing that sample's
main.bicep with its OPERATIONS.md under a floor of four alert specs
(SampleDeploymentObservabilityTests.cs:14-20), so a runbook section that goes missing there fails
here instead of propagating into every repo that lifted it
(SampleDeploymentObservabilityTests.cs:5-11).
Five of the newer bases show how far the pattern has been pushed.
CancellationTokenConventionTestsBase
(Bases/Cqrs/CancellationTokenConventionTestsBase.cs:16) requires every public async method on a public
Application- or Infrastructure-layer type to declare a trailing CancellationToken cancellationToken,
because cancellation is only end-to-end if it is uniform: one method that swallows the token turns a
cancelled request, an expired CQRS timeout budget or a stopping host into work that keeps running
against the database, and a token in a non-trailing position or under a different name defeats the
mechanical forwarding the decorator pipeline and the repositories rely on
(Bases/Cqrs/CancellationTokenConventionTestsBase.cs:3-9), with signatures the repo does not own exempted
automatically (:11-14). NamespaceCycleTestsBase
(Bases/Layering/NamespaceCycleTestsBase.cs:15) requires the top-level namespaces inside each layer assembly
to form a directed acyclic graph, catching the folder pair that has grown into one tangled unit at a
granularity the two coarse layer rules cannot see inside; its own remarks are careful to say the rule
is signature-level reflection blind to method bodies, so green means "no structural cycle", not "no
coupling" (Bases/Layering/NamespaceCycleTestsBase.cs:3-13).
IdempotencyConventionTestsBase
(Bases/Cqrs/IdempotencyConventionTestsBase.cs:10) requires every POST action in a repo's API layer to
state in code whether a retried request replays the original response ([Idempotent]) or
deliberately does not ([NonIdempotent("why")]), so the answer is a compile-time artifact rather
than tribal knowledge (Bases/Cqrs/IdempotencyConventionTestsBase.cs:3-8).
ProtoContractTestsBase (Bases/Contracts/ProtoContractTestsBase.cs:19) is the
synchronous counterpart to IntegrationEventContractTestsBase: it rebuilds a repo's live gRPC
.proto contract and diffs it against a committed snapshot, and it is explicitly consumer-facing,
because MMCA.Common ships the gRPC plumbing rather than any contracts of its own
(Bases/Contracts/ProtoContractTestsBase.cs:3-11).
FolderWidthTestsBase
(MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14, [Rubric §5, Vertical
Slice]) walks a repo root and fails when any folder under Source/ or Tests/ holds more than
MaxDirectFiles direct code files, twelve by default (FolderWidthTestsBase.cs:23), so a folder
keeps naming a feature instead of drifting into a technical bucket
(FolderWidthTestsBase.cs:3-13); a .razor component and its code-behind count as one unit,
resource and generated files do not count at all, and build output plus tool-owned trees (bin,
obj, Migrations, Platforms, Resources, wwwroot) are skipped outright (:8-12). A repo
activates it with a thin subclass supplying its RepoRoot (:20) and, where a flat layout is
documented, its ExemptFolderSuffixes (:29); the single [Fact] delegates to the
ArchitectureRules.FoldersStayNarrow rule body (:31-33), which is how the feature-by-folder
convention of
ADR-109 became a build
gate rather than a review note. Sibling bases pin integration-event contracts
(ADR-010), the
one-upcaster-per-source-contract rule that keeps the upcast chain a function (so which contract a
handler receives cannot depend on DI registration order,
MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:7-14,
ADR-090),
service-contract purity so an extracted service's wire surface
carries only Shared and contract types and never the producer's internals
(MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:13-18,
ADR-007), data residency, forms
conventions, localization resources, concurrency,
controller shape,
state management,
UI architecture, and framework-version consistency, so the
governance-as-tests pattern spans much of the 34-category rubric.
The newest gate in the library is also the one that tests itself.
SliceCohesionTestsBase
(MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:10, tagged to the
vertical-slice rubric category in its own summary, :3-9) asks a repo only for its map and exposes
two facts, one for handlers and one for validators (SliceCohesionTestsBase.cs:14-20), each
delegating to a rule body in
MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Slices.cs: a use-case slice keeps its
command or query, the handler that serves it and its validator in one namespace, the
.../UseCases/{Action}/ folder, instead of stranding the handler in a horizontal Handlers/ folder
away from its contract (ArchitectureRules.Slices.cs:6-9). Two carve-outs keep it honest. Only
same-assembly contracts are checked, because a module reusing a framework-generic command such as
UpdateEntityCommand<,,> is legitimately not co-located with it, while a command the module
derives from that generic is declared in its own assembly and is checked like any other
(ArchitectureRules.Slices.cs:9-15); and the scope stops at the contract, its validator and its
handler, because DTOs/, Specifications/ and a shared Validation/ rule set are aggregate-scoped
by design, so forcing them into one slice's folder would either duplicate them per slice or make one
slice the arbitrary owner the others reach into (ArchitectureRules.Slices.cs:17-25). Since the rule
scans abstract handler bases too, which is the shape a consumer actually derives from
(ArchitectureRules.Slices.cs:28-30), it carries its own self-test:
SliceCohesionFitnessTests
(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:17)
points a fixture map at the test assembly itself (FixtureApplicationMap,
SliceCohesionFitnessTests.cs:70-78) and pins the three shapes that matter: a stranded abstract base
is flagged (StrandedFixtureHandlerBase,
MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11), a base
declared beside its contract is not (GetFixturePreferencesHandlerBase,
MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:19, next to the
GetFixturePreferencesQuery it serves at SliceFixtures.cs:13), and a base parameterized over its
contract is skipped, because a type parameter is not a type any folder can hold
(GetFixtureEntityHandlerBase<TQuery>, SliceFixtures.cs:33, all three asserted at
SliceCohesionFitnessTests.cs:21-53). A fitness rule is production code for the build, and a rule
whose own edge cases are compiled as fixtures is what holding it to that standard looks like.
Component tests: real MudBlazor, faked edges
The bUnit tier renders a single Blazor component in-process with its real dependencies but stubbed
network and auth. BunitComponentTestBase
(MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:38) registers MudBlazor services
and the vendor-neutral toast/confirm facades every migrated page injects
(BunitComponentTestBase.cs:46,53), puts JSInterop in loose mode so MudBlazor components that probe
JS during render do not throw (BunitComponentTestBase.cs:55), then wires a mutable
AuthenticationStateProvider (BunitComponentTestBase.cs:63, the provider class at :167) plus the
real DefaultAuthorizationService (BunitComponentTestBase.cs:62) so both <AuthorizeView>
cascades and pages that inject the provider directly behave. The real service is registered
explicitly because bUnit pre-registers a placeholder that throws and AddAuthorizationCore only
TryAdds, and because it evaluates the requirements a view actually builds (roles, the default
deny-anonymous policy) where a permissive double would authorize every authenticated principal
whatever the view asks for (BunitComponentTestBase.cs:58-61). Tests render
anonymously by default via RenderUnderTest<TComponent> (BunitComponentTestBase.cs:129) or as a
supplied ClaimsPrincipal via RenderAs<TComponent> (BunitComponentTestBase.cs:135, which sets
the provider and the cascading AuthenticationState together at :140-143), with a SetUser hook
(:126) for mid-test auth changes and TestPrincipal
(MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7) minting the authenticated principal: a
name claim, the user identifier written twice (under sub and under NameIdentifier, because a
real principal reaches a page under either name), the requested roles, and an authentication type so
IsAuthenticated is true (TestPrincipal.cs:22-32, rationale at :14-21). RenderMudProviders
(BunitComponentTestBase.cs:153) mounts the popover, dialog, and snackbar providers and returns them
as a MudProviderHandles record (BunitComponentTestBase.cs:162) so
components that open a dialog or raise a toast have somewhere to render. One helper is worth knowing
before you write your first list-page test: ConfigureDataGridListPageHost
(BunitComponentTestBase.cs:105) wires the list-page state services, an inert viewport double so
IsMobile is deterministic (:108-111), persistent component state (:114), and SetRendererInfo
last (:116-117), because SetRendererInfo freezes the bUnit service provider and every
registration made after it is silently ignored (:77-83). The class is pinned to bUnit v2 (the line
compatible with xUnit v3 and Microsoft Testing Platform) and isolates every version-specific symbol
here so a bUnit change touches only this file (BunitComponentTestBase.cs:29-34). Localization is
pre-registered (AddLocalization, BunitComponentTestBase.cs:63-64) so components injecting
IStringLocalizer<T> (ADR-027)
render without per-test setup, a [Rubric §27, i18n] touch. Test bodies then read as user actions
rather than DOM queries through
BunitInteractionExtensions
(MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12), a generic
extension<TComponent>(IRenderedComponent<TComponent>) block (:14) offering FindButtonByText
(:18, which throws listing every button present when nothing matches), ClickButtonByText (:28),
and HasText (:32), all keyed on accessible text rather than brittle CSS paths.
HTTP-backed UI services are exercised without a server through
CapturingHttpMessageHandler
(MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:20), a canned-response,
request-recording HttpMessageHandler supporting both a responder delegate
(CapturingHttpMessageHandler.cs:39) and route registration (SetResponse,
CapturingHttpMessageHandler.cs:49), with registered routes consulted first and unmatched requests
returning 404 to mirror the WebAPI's not-found behavior
(CapturingHttpMessageHandler.cs:7-17,119-137); it rebuilds each response fresh so a Polly retry
never reuses a consumed HttpContent (CapturingHttpMessageHandler.cs:141-150), and records every
request as a CapturedRequest (CapturingHttpMessageHandler.cs:158) against a
registered Route (CapturingHttpMessageHandler.cs:139), flattening request and content
headers into one case-insensitive lookup so a caller asserting If-Match or Content-Type does not
have to know which half carries it (:94-117).
UiHttpServiceHarness
(MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12) wraps that handler with a
FreshApiClientFactory (UiHttpServiceHarness.cs:73) returning a fresh
client per call, which is load-bearing because the UI services dispose the client after each request,
so a caching factory would hand later calls a disposed one (UiHttpServiceHarness.cs:66-72), plus a
fixed-token StubTokenStorageService
(MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13, constructed and exposed at
UiHttpServiceHarness.cs:48,61), all on a https://gateway.test/ base address
(UiHttpServiceHarness.cs:15). Tests that would rather wire the pieces individually reach for
HttpTestDoubles
(MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12), which exposes the same factory and
token stub standalone (:23,28) alongside the canned JsonResponse/EmptyResponse/ProblemResponse
builders (:33,37,47) that reproduce the shapes the WebAPI actually emits.
MarkupSnapshot (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:22)
adds dependency-free golden-markup regression testing: Match (MarkupSnapshot.cs:31) normalizes
the per-render GUIDs MudBlazor injects (MarkupSnapshot.cs:64-70), compares against a committed
baseline under Snapshots/ next to the calling test (located through a [CallerFilePath] argument,
MarkupSnapshot.cs:31,37), and returns a MarkupSnapshotResult
(MarkupSnapshot.cs:104) for the caller to assert on, which keeps the shipped package free of an
assertion-library dependency (MarkupSnapshot.cs:10-12). UPDATE_SNAPSHOTS=1 rewrites baselines
(MarkupSnapshot.cs:41-46) and a missing baseline is written but reported as a non-match so a
regression cannot slip through on an absent snapshot (MarkupSnapshot.cs:48-54). Whole page
suites ship on top of that base for the framework-owned pages of
ADR-116:
ConfirmEmailPageTestsBase
(MMCA.Common.Testing.UI/Pages/ConfirmEmailPageTestsBase.cs:26) pins the anonymous
/confirm-email page (a complete link redeemed once on arrival, an incomplete one landing on manual
entry, a resend that answers the same whether or not the address has an account,
ConfirmEmailPageTestsBase.cs:13-19), while
RoleAdminListPageTestsBase<TPage>
(Pages/RoleAdminListPageTestsBase.cs:21) and
RoleAdminEditPageTestsBase<TPage>
(Pages/RoleAdminEditPageTestsBase.cs:22) pin the consumer's own half of the routed shells over the
shared role roster and permission editor: route parameters, links back and forth, the compiled
permission locked, and a save that submits the stored set (RoleAdminEditPageTestsBase.cs:13-19).
Each is activated by a one-line sealed subclass in the consuming repo, the ADR-015 pattern carried
into the component tier. This tier is
[Rubric §28, Front-End Testing] and [Rubric §18, UI Architecture].
End-to-end tests: a real browser, accessibility and performance as gates
The E2E tier drives a real browser through Playwright. PlaywrightFixture
(MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6) is an xUnit collection fixture (its
E2ETestCollection definition sits beside it at PlaywrightFixture.cs:47-51,
the class itself at :48) that launches the engine selected from configuration, chromium,
firefox, or webkit, with unknown values falling back to Chromium (PlaywrightFixture.cs:17-22).
That environment-selected engine is what lets CI run the same suite as a cross-browser matrix,
[Rubric §22, Responsive/Cross-Browser]: MMCA.Common's ui-e2e job is a three-engine matrix
(MMCA.Common/.github/workflows/ci.yml:236-237) with E2E_BROWSER handed to each leg (:298).
Headless mode, slow motion, base URL, timeouts, trace capture, and the seeded admin/user credentials
all come from E2ETestConfiguration
(MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:15), whose nested
AdminCredentials (E2ETestConfiguration.cs:80) and
UserCredentials (E2ETestConfiguration.cs:92) let a downstream project set
app-specific defaults through a [ModuleInitializer] while environment variables always win. Two of
its knobs exist purely to de-flake CI: AuthTimeout (E2ETestConfiguration.cs:34) tunes the slowest
step, the post-auth round-trip, independently of the 30-second general default (:25-26), and
AuthGraceTimeout (E2ETestConfiguration.cs:45, default 15 seconds at :46) gives the success
signal a window to appear after a transient error alert flashes during the success-path reload.
E2ETestBase (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9) is the
per-test base on top of that fixture. It opens a fresh browser context per test with
IgnoreHTTPSErrors and the configured base URL (E2ETestBase.cs:20-37), optionally records a
Playwright trace (:32-35) and, when E2E_TRACE names a directory, keeps only the traces of tests
that failed so a full-suite run yields exactly the inspectable failures (:68-90, the failed-only
branch at :79-85). Its LoginAsync (E2ETestBase.cs:98) clears both token stores before signing
in (localStorage for the WASM host and the HttpOnly session cookie for the Server host, :100-114),
and WaitForAuthResultAsync (:214) races three signals so success detection does not depend on the
logout button having hydrated (:219-228, rationale at :206-213). The verdict itself is not
inline: it is a pure function, AuthOutcomeRules.Classify
(MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:38, invoked at E2ETestBase.cs:236-239),
which returns an AuthOutcome (AuthOutcomeRules.cs:4) of Succeeded,
ErrorShown, or Silent, so the classification is testable without a browser and the silent case
(no navigation and no rendered error) can no longer be mistaken for success
(AuthOutcomeRules.cs:19-26); anything other than success gets the grace window once
(E2ETestBase.cs:248, AuthSucceededWithinGraceAsync at :282). ScanAsync (:334) and
ScanGridAsync (:324) wrap the accessibility gate for settled pages and for MudDataGrid list pages
respectively: the grid variant waits for a data row and for the loading bar to disappear before
scanning and applies the pager carve-out (:326-329), while the plain scan stays fully strict on
WCAG 2.1 AA (:336-337).
The hard part of Blazor E2E is timing, and PageExtensions
(MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23) is where that knowledge is
centralized, as C# extension(IPage) and extension(ILocator) blocks (PageExtensions.cs:62,335,
see primer §4). The app uses InteractiveAuto with
prerendering, so a page appears as static HTML before the runtime wires its event handlers, and the
runtime flag alone is a false green. WaitForBlazorAsync (PageExtensions.cs:85) therefore waits in
two phases: first for window.Blazor._internal (:39), which only says the WASM CLR or the SignalR
circuit is ready, then for the data-mmca-interactive attribute that MmcaThemeProviders stamps on
the document element from its first interactive render (:47-48), which is the honest gate, and
finally flushes one animation frame (:60,96). GotoAndWaitForBlazorAsync (:103) pairs navigation
with that wait and deliberately settles on Load rather than NetworkIdle, because the persistent
SignalR WebSocket means network idle never arrives (:106-108); BlazorNavigateAsync (:118)
routes client-side so a protected page is not re-prerendered without its token, polling
window.location instead of WaitForURLAsync because a same-document navigation fires no load event
(:132-138); and GotoProtectedAsync (:160) probes the same runtime predicate and loads a public
page first when the runtime is not yet up (:167-188). FillAndVerifyAsync (:347) fills a field
then auto-waits until the value sticks, retyping character by character if hydration wiped it
(:353-365), and ClickAndVerifyAsync (:380) and ClickAndWaitForUrlAsync (:423) retry a click
until its visible effect appears so a click that beats hydration is not silently swallowed
(:389-410). List-page helpers built on the same primitives (SearchAndWaitForRowAsync :233,
ConfirmDeleteAsync :266, WaitForGridToSettleAsync :293) keep grid interactions out of every
page object. These helpers encode hard-won lessons about the prerender and hydration race and are
shared by every page object.
Accessibility and performance are asserted here, not deferred to a separate audit.
AssertNoAccessibilityViolationsAsync (PageExtensions.cs:307) runs an axe-core scan and throws
AccessibilityViolationException
(MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7) with a compact
per-node summary of every violation (PageExtensions.cs:320-331), so an inaccessible page fails the
build, [Rubric §21, Accessibility]. The scan scope itself is shipped as AxeOptions
(MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9): Wcag21Aa (AxeOptions.cs:17) pins the
documented target of WCAG 2.1 AA tags (AxeOptions.cs:19-23) and deliberately excludes axe's
advisory best-practice rules, and Wcag21AaExceptMudPagerCombobox (AxeOptions.cs:35) is the one
documented carve-out, disabling only aria-input-field-name for MudBlazor 9.6.0's unlabeled pager
select (AxeOptions.cs:26-33,42-45). WebVitalsCollector
(MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20) installs
PerformanceObserver-based Core Web Vitals capture (LCP, CLS, FCP, TTFB, INP) as an init script
before first paint (InstallAsync, WebVitalsCollector.cs:40, script at :26-35), reads the
accumulated values back as a WebVitalsSample (CollectAsync,
WebVitalsCollector.cs:47,76), and writes a citable JSON artifact under WEB_VITALS_OUTPUT_DIR
(WriteArtifactAsync, WebVitalsCollector.cs:63, artifact record
WebVitalsArtifact at :90) for CI, with
WebVitalsBudget (:103) as the shared assert mechanics defaulting to the Core
Web Vitals "good" band, LCP 2500 ms / FCP 1800 ms / TTFB 800 ms / CLS 0.1 / INP 500 ms
(WebVitalsCollector.cs:104-108), and skipping the INP assertion when no interaction cleared the
16 ms threshold (:148-151), [Rubric §23, Front-End Performance] (the source tags it rubric §12).
LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and the observers fail
silently rather than throwing (WebVitalsCollector.cs:14-16,22-25).
Five reusable identity page objects ship with the package: LoginPage
(MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6), RegisterPage
(MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6), ProfilePage
(MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6),
ForgotPasswordPage
(MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6), and
ResetPasswordPage
(MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6). They wrap the framework's real auth
surfaces with role- and label-based locators (LoginPage.cs:12-18) and route their own fills through
the anti-race helper (LoginPage.cs:31-32, invoked at :25-26). A sixth,
RoleAdminPage (MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:18),
drives the ADR-116 role roster at /roles and editor at /roles/{role} purely through the
data-testid and data-permission attributes the shared components render, so a MudBlazor upgrade
cannot break it (RoleAdminPage.cs:5-17). For eventually-consistent state inside a browser test,
E2EPolling (MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:18) is the
package's counterpart of the integration tier's TestPolling (the E2E package does not reference
MMCA.Common.Testing): PollUntilAsync (E2EPolling.cs:38,70) returns as soon as the condition
holds, bounded by a 30-second deadline at a 500 ms interval by default (E2EPolling.cs:21,24), and
serves only "until present" waits (E2EPolling.cs:13-16). Downstream apps add their own
family, for example the 45 page-object classes under MMCA.ADC.E2E.Tests/PageObjects/ covering
events, sessions, speakers, rooms, questions, feedback, sponsors, and the QR check-in and points
surfaces. Whole workflows ship too, not just page objects: eight abstract suites under
MMCA.Common.Testing.E2E/Workflows/ (six identity flows, AuthorizationTestsBase,
UserLoginTestsBase, UserRegistrationTestsBase, LogoutTestsBase, ProfileManagementTestsBase
and PasswordResetTestsBase, plus
UserPreferencesTestsBase
(Workflows/Preferences/UserPreferencesTestsBase.cs:21) and
PseudoLocalizationTestsBase
(Workflows/Globalization/PseudoLocalizationTestsBase.cs:52)) are authored once and re-run per
consumer. Their shape is the same supply-only-your-facts contract as the fitness bases:
AuthorizationTestsBase
(MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18) asks the subclass only
for its route lists (ProtectedPaths :26, PublicPaths :29, optional AuthenticatedUserPath
:35 and AdminPaths :44) and owns the assertions, including the non-empty guard that keeps the
anonymous-redirect check from passing vacuously (:49-50).
PasswordResetTestsBase
(MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17) shows where the tier
draws its own boundary: it asserts the recovery flow is reachable from the login page (:24-41) and
that an unknown address produces the identical anti-enumeration confirmation (:43-58), but
deliberately does not consume a real reset token, because that token only reaches the user by email
and so belongs to an app-side integration test (:10-16).
The Gallery harness
Component and E2E coverage of MMCA.Common's own UI needs a page to render, but the framework is not
a runnable app. MMCA.Common.UI.Gallery is a deliberately backend-less Blazor host that renders the
real MMCA.Common.UI auth pages (/login, /register), the shared notification pages, and a
primitives showcase (/components), so a real-browser axe scan can run inside MMCA.Common's own CI
(GalleryHost, MMCA.Common.UI.Gallery/GalleryHost.cs:24, host built by
BuildApp at :31). It is kept outside MMCA.Common.slnx (together with
MMCA.Common.UI.E2E.Tests) so the unit-test run stays fast; the CI ui-e2e job builds that
out-of-slnx graph directly by csproj path and runs the axe plus render smoke against the gallery
(MMCA.Common/.github/workflows/ci.yml:223-227,260-264,293-306). Because the E2E suite self-hosts it
in-process, where the entry assembly is the test host and the environment is Production, the host
also has to point the static-web-assets loader at the gallery's own runtime manifest and force it on,
otherwise the auth pages render unstyled, never become interactive, and axe's contrast checks are
meaningless (GalleryHost.cs:36-50).
The host runs without a backend by registering stubs before AddUIShared so its TryAdd*
registrations defer to them (GalleryHost.cs:57-65, AddUIShared at :102):
NoOpAuthUIService
(MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14),
NullTokenStorageService
(MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11),
NullTokenRefresher
(MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9), and
GalleryAuthenticationStateProvider
(MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16), plus canned notification
services (StubNotificationInboxUIService
(Stubs/StubNotificationInboxUIService.cs:11),
StubPushNotificationUIService
(Stubs/StubPushNotificationUIService.cs:11)) so the bell and the inbox render populated markup
(GalleryHost.cs:77-82), the framework's
NullNotificationScopeProvider for
the unscoped send page (GalleryHost.cs:87), and one empty
GalleryUIModule (Stubs/GalleryUIModule.cs:14, registered at
GalleryHost.cs:97) so the shared Router discovers the gallery's own page alongside the real ones.
Because the notification pages carry a real [Authorize] that MapRazorComponents surfaces as
endpoint metadata, the gallery also needs a genuine authentication scheme:
GalleryFakeAuthenticationHandler
(MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21, registered at
GalleryHost.cs:67-75) authenticates only requests carrying the gallery_auth=1 cookie
(GalleryFakeAuthenticationHandler.cs:26,30-39), so the guarded pages are scanned signed in while
/login, /register, and /components are scanned in their deliberate anonymous state
(GalleryFakeAuthenticationHandler.cs:8-18). The gallery is also where the i18n evidence is
produced: it enables the qps-Ploc pseudo-locale unconditionally (GalleryHost.cs:104-112), which
real hosts keep Development-only, because the pseudo-localization pass is a required CI gate for
[Rubric §27, i18n] and this host is unpackaged test infrastructure that is never deployed.
Evaluating a nondeterministic dependency: the AI-scoring golden corpus
Every tier above assumes a deterministic system under test. ADC's session-scoring judge is not one:
it asks a hosted model for a structured score, and the same prompt can return a different number on
a different day. MMCA.ADC.Conference.Scoring.Evaluation.Tests is the tier built for that case, and
it is the subject of
ADR-111. The
corpus is a folder of committed JSON files copied next to the test assembly.
GoldenCase
(MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenCase.cs:18)
is one case: a session proposal, a stable id, a one-line note on what it pins, and the band its
overall score has to land in. The recorded answer is no longer part of the record: it lives beside
the case as Golden/<id>.response.json, written by the framework's
RecordedResponses in the ChatResponse
shape rather than a provider's wire format, so a provider swap cannot invalidate the corpus
(GoldenCase.cs:7-17). GoldenCorpus
(GoldenCase.cs:106) loads every case-*.json from the copied Golden/ folder ordered by id so a
run is deterministic (GoldenCase.cs:118-129), and throws when it finds none, so a csproj that
stopped copying the corpus fails loudly instead of passing on zero cases (GoldenCase.cs:131-134);
it also exposes the Golden/prompt-versions.json path that maps a prompt version to its recorded
hash (GoldenCase.cs:114-116).
Three suites read that corpus, and they fail for three different reasons.
GoldenReplayTests (GoldenReplayTests.cs:35) is the regression half, a
subclass of the framework's
GoldenReplayTestsBase: every case runs
through the real SessionScoringService against a fresh
ReplayChatClient that returns the response
recorded for it, so no API key and no network are involved, it runs on every CI leg, and the base
collects every broken case instead of stopping at the first (GoldenReplayTests.cs:15-25). The
replay client is the innermost provider under the host's own governed pipeline, composed through the
same AddConferenceAiGuardrails call the Conference service makes, and the test asserts a
GuardrailChatClient really is in front of it, so a guardrail that would change a production score
fails here instead of shipping (GoldenReplayTests.cs:49-61,165-183). It pins both directions at
once, what goes out (the delimited envelope and the untrusted-input brief on the request, including
for the cases with no speakers and with an injection attempt, plus proof that request-side redaction
stripped the contact details from the contact-details-in-bio case before they reached the
provider) and what comes back (a recorded response still parses, still succeeds, and still produces
the same weighted overall inside the case's band), so a change to prompt assembly or to the
weighting math fails here with the case that noticed it named (GoldenReplayTests.cs:26-32,67-80,143,160).
LiveJudgeTests (LiveJudgeTests.cs:39) is the live half, scoring the same
proposals through the real provider to catch what a replay cannot: a model deprecation, a
structured-output contract change, a prompt edit that reads fine but scores everything a point
lower. It composes the client exactly as the host does, from the host's deployed Ai section and
the provider adapter named by Ai:Provider, so it names no vendor type and keeps no guardrail list
of its own (LiveJudgeTests.cs:23-30). It costs money and needs a key, so it is trait-gated
Category=AiEval.Live and skips itself when AI_API_KEY is absent (LiveJudgeTests.cs:17-21,41,59). Its bands are deliberately
generous, roughly plus or minus 1.5 around the recorded value and clamped to the 1.0 to 10.0 range,
because a judge model is not deterministic even at the same prompt and a tight band would produce a
flaky gate that gets ignored, which is worse than no gate: what the bands catch is a shift, not a
wobble (LiveJudgeTests.cs:32-36).
PromptContractTests (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:29) closes the loop that
makes a persisted score meaningful. Scores are stored with the prompt version that produced them,
which is worth something only if the version actually moves when the prompt does, so this suite
renders the system brief plus the user message for one canonical proposal, hashes it, and compares
against Golden/prompt-versions.json: a prompt edit without a version bump fails because the
recorded hash for the current version no longer matches, and a version bump with no recorded hash
fails too (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:16-22). The canonical proposal is fixed in the test file rather
than read from the corpus on purpose (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:37-45), because editing a golden case
must not be able to change what the contract hash covers (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:25-26); it is
chosen to exercise every branch of the prompt assembly, with a description and two speakers, one of
them with no tagline and no bio (MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:34-35). Beside it in the same file,
SessionScoringPromptContractPinTests
(PromptContractTests.cs:180) runs the framework's own prompt-change protocol,
PromptContractPinTestsBase, over
the scorer's one contract: it pins the normalized contract hash (name, version, model and system
brief) in the same Golden/prompt-versions.json under a session-scoring@<version> key, while the
bare <version> keys stay the rendered-prompt hashes PromptContractTests reads, and each base
ignores the keys it does not own (PromptContractTests.cs:166-187). Together these suites are
[Rubric §14, Testability] applied to a dependency that cannot be asserted exactly, and [Rubric §29,
Resilience & Business Continuity] on the operational side: the offline gate is free and always on,
the paid gate is opt-in, and neither one can pass vacuously.
The framework side of the same dependency is tested differently, because what MMCA.Common ships is
pipeline mechanics rather than a model. MMCA.Common.AI.Tests pins when the pieces run and what
they do, against stub edges rather than a hosted model:
GuardrailChatClientTests
(MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/GuardrailChatClientTests.cs:12) holds the guardrail
layer to being an extension point and not a policy (ADR-120), asserting when an application's
guardrail runs and what a block does, never what a guardrail should consider unsafe
(GuardrailChatClientTests.cs:7-11), driven by a StubGuardrail whose request,
response and per-update verdicts the test supplies and which counts its request, response and
streamed-update inspections
(MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Fixtures/StubGuardrail.cs:10-13,16,19,22), so the
pipeline tests need no real policy. The shipped policies are asserted from the innermost client's
point of view, because what the provider was actually sent is the only place they count:
ContentPolicyGuardrailTests pins both what a marker costs an
attacker and which benign sentences must still pass
(MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Guardrails/ContentPolicyGuardrailTests.cs:7-12),
RequestRedactionTests that redaction reaches the provider without
rewriting the caller's own messages (Guardrails/RequestRedactionTests.cs:8-14),
ToolPolicyTests that a tool the policy removed is absent from what the model
was offered (Guardrails/ToolPolicyTests.cs:10-15), and
StreamedGuardrailTests that every streamed update is inspected before
it is yielded (Guardrails/StreamedGuardrailTests.cs:7-12). The evaluation harness ADC's corpus
runs on is held to the same standard before anything it gates is trusted:
ReplayChatClientTests pins the replay client and the golden base so
neither can answer with the wrong recording or pass on an empty corpus
(Evaluation/ReplayChatClientTests.cs:6-13),
RecordedResponsesTests pins the two on-disk formats and the exact
failure messages of the prompt pin (Evaluation/RecordedResponsesTests.cs:6-12), and
ReferenceGoldenReplayTests drives a tiny reference summarizer
through GoldenReplayTestsBase against three recorded answers so the shipped base runs on every
Common pull request, not only in the repos that consume it
(Evaluation/ReferenceGoldenReplayTests.cs:6-14). On the ADC side,
ConferenceAiGuardrailsRegistrationTests guards the one
guardrail list the Conference host and both evaluation tiers share
(MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/ConferenceAiGuardrailsRegistrationTests.cs:12-17),
SessionScoreResponseGuardrailTests asserts the answer rules
at the pipeline extension point where they now live, a blocked answer returning as an ordinary
failed score (Services/SessionScoreResponseGuardrailTests.cs:11-18), and
SessionScoringServiceTests talks to a recording stub
IChatClient rather than a fake provider HTTP handler, leaving token-cost counters to the
framework's own meter (Services/SessionScoringServiceTests.cs:13-22);
UsageRecordingChatClientTests does the same for the token-usage
recorder, collecting what was emitted through a DurationMeasurement record declared in the test
file itself
(MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/UsageRecordingChatClientTests.cs:212). One fact in
that family sits deliberately outside the AI package:
AiTelemetryExportTests
(MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/AiTelemetryExportTests.cs:20)
asserts that the meter and the activity source named MMCA.Common.AI really are exported by the
Aspire defaults, which subscribe that name as a literal string because
AiDependencyIsolationTestsBase bars a project reference from the Aspire package to the AI one, so
a rename on either side goes red here instead of quietly taking model-spend telemetry off every
dashboard (AiTelemetryExportTests.cs:12-19,22).
Contract, pipeline, and benchmark bases
The last family pins guarantees that live in the composition of the stack rather than in any one
type, and it is the subject of
ADR-058:
these suites ship in MMCA.Common.Testing as abstract behavioral bases and every one of them runs
against a host that was actually booted. SecurityHeadersTestsBase
(MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16, [Rubric §11, Security] and [Rubric §26,
Front-End Security]) probes an always-responding endpoint (ProbePath, default /alive,
SecurityHeadersTestsBase.cs:19) and asserts the hardened header set: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, a
Permissions-Policy containing geolocation=(), a Content-Security-Policy containing
frame-ancestors 'none', and, in the Production environment, HSTS
(SecurityHeadersTestsBase.cs:29-35). Its siblings
OpenApiContractTestsBase<TFixture>
(MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32, with a MinimumPathCount floor at :37, a
pinned CorePublicResources list at :50, and deliberately no committed snapshot file so a new
controller can never leave a stale baseline behind, :14-16),
ProblemDetailsContractTestsBase<TFixture>
(MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22, asserting the RFC 9457 shape across
both error-shaping paths, model validation at :30 and the framework's HandleFailure mapping at
:42, through one shared AssertProblemDetailsShapeAsync at :67), and
ServiceInfoVersioningContractTestsBase<TFixture>
(MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20, driving /ServiceInfo at both
api-version: 1.0 and 2.0 and checking the deprecated/supported reporting headers at :38,:54)
all subclass IntegrationTestBase<TFixture> and pin the
corresponding API contracts, [Rubric §9, API & Contract Design]. The non-HTTP member of the family is
GracefulShutdownTestsBase<TEntryPoint>
(MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25, [Rubric §29, Resilience & Business
Continuity]): it boots a host through ProductionHostApplicationFactory, calls a real
IHost.StopAsync under a bounded token defaulting to 20 seconds (:28,:55-56), and asserts
ApplicationStopping then ApplicationStopped fired (:58-61). The failure it catches, a hosted
service that refuses to drain, is invisible in production until it wedges a rolling deploy.
Four bases guard the composition of the pipelines themselves.
DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>
(MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38) is the opt-in fitness function for
ADR-014: it builds a real
ServiceCollection through the repo's own registration sequence (ConfigureServices, :46),
resolves the decorated handlers, unwraps each decorator's private inner-handler field by reflection
so it verifies the constructed object graph rather than the registration list (:105-125), and
asserts the runtime nesting is exactly FeatureGate, Authorization, Logging, Caching, Validating,
Timeout, Transactional, handler for commands (DecoratorPipelineOrderTestsBase.cs:49-58) and
FeatureGate, Authorization, Logging, Caching, Validating, Timeout, handler for queries (:61-69),
through the two [Fact]s at :71-77, with a final check that the innermost element is not itself a
decorator (:96-97). Authorization and Timeout are the newest links in both chains, inserted by
ADR-014's 2026-08-18 revision
(Website/docs-src/adr/014-cqrs-decorator-pipeline.md:96-107); note that the revision's query line
(:107) omits Validating, while the shipped registration and this base both include it
(MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:144), so the code is the
authority here. Because Scrutor's TryDecorate applies decorators in reverse registration order, an
innocent-looking reorder of the AddApplicationDecorators() lines silently changes runtime behavior,
and this base turns that into a test failure (see group 5).
MiddlewarePipelineOrderTestsBase
(MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31) is its counterpart on the HTTP edge:
it seeds MiddlewarePipelineBuilder.CreateDefault, applies the host's own Configure customization
when it has one (:35), and asserts the eighteen-step order from the exception handler down to the
controller endpoints (:38-58). Several adjacencies there are load-bearing (the pre-forwarded
capture immediately before UseForwardedHeaders, authentication immediately before tenant
resolution, authentication before the rate limiter per
ADR-019, forwarded headers before the
HTTPS redirect), and a reorder that breaks one of them fails at runtime looking like a configuration
bug: an unreachable jwks_uri, a tenant that never resolves, a per-user rate cap that never engages
(:13-18). Unlike the contract bases it needs no host at all, because the steps are pure data until
they are applied, so it runs in the fast unit tier (:24-27).
DependencyInjectionAssert
(MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13) guards the other half of that composition:
ReturnsSameCollection (:21) asserts a registration extension hands back the very
IServiceCollection it was given (:29-31), because an extension that returns a new collection
silently drops every registration chained after it and nothing else catches services that are simply
absent (:6-11). HandlerTestBase<THandler>
(MMCA.Common.Testing/Support/HandlerTestBase.cs:38) is the fast unit-tier counterpart for exercising a
single handler without a host: it owns a Mock<IUnitOfWork> whose SaveChangesAsync is
pre-configured to succeed (HandlerTestBase.cs:41-42,45), a NullLogger<THandler> (:48), and
RegisterRepository<TEntity, TIdentifierType>() (:56) / RegisterReadRepository<TEntity, TIdentifierType>() (:72) helpers that wire a repository mock into the read and write accessors
(the read-only variant exists for child entities, which expose no read-write repository).
A smaller final tier measures rather than asserts behavior. MMCA.Common.Benchmarks
(BenchmarkDotNet) covers the per-request query pipeline, where the dynamic-LINQ predicate is
re-parsed per call and the shaper reflects over DTO properties
(QueryPipelineBenchmarks,
MMCA.Common.Benchmarks/QueryPipelineBenchmarks.cs:9-17), and the specification hot path
(SpecificationBenchmarks,
MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:8-14), both with [MemoryDiagnoser] allocation
tracking. Its results are compared in CI by build/perfgate against the committed
MMCA.Common/Tests/Performance/perf-baseline.json
(MMCA.Common/.github/workflows/ci.yml:397), so moving a number has to be a deliberate, reviewed
change, [Rubric §12, Performance & Scalability]. The same job family carries one more quiet gate
worth knowing: the unit run is invoked with --minimum-expected-tests 2000
(MMCA.Common/.github/workflows/ci.yml:161), so a discovery regression that silently drops thousands
of tests fails the build instead of reporting a green, empty run.
Benchmarks time one operation in isolation; MMCA.Common.LoadTests measures the framework at volume.
LoadStack (MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/Support/LoadStack.cs:25)
wires the real persistence stack exactly as a host does, AddApplication() plus
AddInfrastructure(configuration), over one temp-file SQLite database, substituting nothing below the
service registrations (LoadStack.cs:13-19); it is a file rather than :memory: because each
scenario opens one connection per scope, as a deployed host does (LoadStack.cs:20-24). Two suites
run on it. OutboxThroughputLoadTests
(MMCA.Common.LoadTests/OutboxThroughputLoadTests.cs:20) enqueues 10,000 integration events as the
framework writes them and lets the real OutboxProcessor, resolved as a hosted service with its
default settings, drain them to a CountingMessageBus: every message must
be delivered exactly once and stamped processed, above a floor of 500 messages per second
(OutboxThroughputLoadTests.cs:12-19,22,30). PagedQueryLoadTests
(MMCA.Common.LoadTests/PagedQueryLoadTests.cs:13) drives EntityQueryService.GetAllAsync over
100,000 rows seeded by PagedQueryFixture, asserting exact rows rather
than a count (so a fast wrong answer still fails) under per-shape median latency ceilings and a p95
ceiling across 50 concurrent paged reads (PagedQueryLoadTests.cs:7-12,23-31). Each constant carries
the local measurement it was derived from, with about 3x headroom over the worst of 25 runs. The tier
is deliberately outside ci.yml and not a required check: the project sits outside
MMCA.Common.slnx, so the solution-wide test run never discovers it, and a timing tier on shared
runners is a trend signal rather than a merge gate, so load-tests.yml runs it weekly on a schedule
plus manual dispatch (MMCA.Common/.github/workflows/load-tests.yml:3-17).
The takeaway for a new engineer: pick the tier that matches what you are proving (a fast unit test
for domain logic, bUnit for a component, an integration fixture for a full request path, a
cross-service fixture for a real broker round-trip, an AppHost collection for the orchestration
itself, an E2E page object for a browser flow, a
*TestsBase subclass for an architectural invariant, a contract base for a runtime guarantee of the
composed host, a golden case for a change in what the scoring model returns, a benchmark for an
allocation budget), and, for every tier but the ADC-local model-evaluation suite, the reusable base
you need is already in one of the five MMCA.Common.Testing.* packages. Adoption is opt-in per host in both governance
tiers, which is the standing caveat in ADR-015 and ADR-058 alike: the framework ships the gate, a
host gets it only once someone writes the subclass
(Website/docs-src/adr/015-architecture-fitness-functions.md:68-70,
Website/docs-src/adr/058-runtime-conformance-suites-as-a-package.md:173-175). Every remaining
concrete test class is cataloged by project in the companion per-project test rollup for this
chapter.
ContractRow
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:330· Level 0 · record (private, sealed, nested)
- What it is - one parsed data row of the
## Route Contracttable inadc-NavigationFlow.md: the backticked route template, the page cell and the access word, as three strings (NavigationContractTests.cs:330). - Depends on - nothing but the BCL; it is a positional record nested inside NavigationContractTests.
- Concept introduced, a row as a value. Parsing the markdown table once into a value type lets every fact compare rows by
Route,PageandAccessinstead of re-splitting text.ReadContractTablebuilds it from exactly three trimmed cells (:243, constructed at:268-:278of the table reader). - Where it's used - only inside the defining file:
ContractTable.Rowscarries it, andEveryContractAccess_MatchesThePageGuardandRouteContractTable_IsWellFormedread it (:79,:107).
PageRoute
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:328· Level 0 · record (internal, sealed, nested)
- What it is - one routable ADC page as discovered from the source tree:
Route(the@pagetemplate),RelativePath(the.razorfile),PageName(file name without extension),Access(Anonymous,AuthenticatedorOrganizer, null when unclassifiable) andViolation(why classification failed, else null) (NavigationContractTests.cs:328). - Depends on - nothing but the BCL; nested in NavigationContractTests.
- Concept introduced, carry the failure with the fact.
Classifyreturns(Access, Violation)andDiscoverPageRoutesstores both on the record (:209,:139), so the guard-presence fact can report a page with no or several access attributes without a second scan. - Where it's used - the four facts and the table-hygiene check in the defining file consume
DiscoverPageRoutes()results (:38-:107). The record isinternalrather than private becauseDiscoverPageRoutesis nowinternal static(:332) andNavItemGuardContractTests.DiscoverGuardedPagesreuses it to map each page to a GuardedPage (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:318-:324).
PageGuard
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:464· Level 0 · record (private, sealed, nested)
- What it is - the normalized access requirement of one page, as five fields:
RequiresAuthentication,AnyOfRoles(the roles of which a caller needs one),ClaimType,Permissionand a human-readableDescriptionsuch as[Authorize(Roles = RoleNames.Organizer)](NavItemGuardContractTests.cs:464). - Depends on - nothing but the BCL; nested in
NavItemGuardContractTests. - Concept introduced, a guard as a comparable value. Every page guard form (
[AllowAnonymous],[Authorize], roles, a policy resolved to the claim or permission it enforces) reduces to this one shape (:321-:323,:354,:403), soCompare(:108) can test a nav item against it with four independent checks: signed-in, role, claim, permission (:117-:135). - Where it's used - only inside the defining file, held by GuardedPage.
GuardedPage
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:462· Level 1 · record (private, sealed, nested)
- What it is - one routable page paired with its guard:
Route,Source(the.razorpath for ADC pages, the component's full type name for framework pages),Guard(a PageGuard, null when unreadable) andProblem(why it was unreadable) (NavItemGuardContractTests.cs:462). - Depends on - PageGuard and, for ADC pages, PageRoute from
NavigationContractTests.DiscoverPageRoutes(). - Concept introduced, carry the failure with the fact. Like
PageRoute, an unreadable guard is data, not an exception:DiscoverGuardedPages(:316) builds a nullGuardplus theViolationfor an unclassified ADC page (:324), andCompareturns that into a violation (:110-:113) rather than skipping the page, so the gate cannot pass over a page it failed to read. ADC pages come from source text (:318-:324); the framework's routable components in MMCA.Common.UI are added by reflection (:333). - Where it's used - only inside the defining file:
EveryNavItem_OpensARoutablePage(:75) matches nav hrefs against itsRoute, andEveryNavItem_IsNoLooserThanTheGuardOfThePageItOpens(:89) compares item againstGuard.
ContractTable
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:332· Level 1 · record (private, sealed, nested)
- What it is - the parse result of the route-contract table:
Rows(aList<ContractRow>) plusProblems(aList<string>of structural defects found while reading) (NavigationContractTests.cs:332). - Depends on - ContractRow, nested in NavigationContractTests.
- Concept introduced, parse problems are data.
ReadContractTable(:243) never throws on a malformed table. A missing## Route Contractheading, a missing| Route | Page | Access |header, a row that is not three cells with a backticked route, or an empty table each append toProblemsand return early with an emptyRows.RouteContractTable_IsWellFormedthen surfaces them (:107). - Where it's used - only inside the defining file;
ReadContractTable().Rowsfeeds three facts and.Problemsfeeds the hygiene fact.
CostTagConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:10· Level 1 · class (public, sealed)
- What it is - the cost-attribution gate over ADC's
infra/main.bicep: every container app must carry aservicetag, and every per-service database must carry the matching one, so per-service spend is readable straight off the Azure cost report. It is a thin subclass ofCostTagConventionTestsBasethat supplies two values,MinimumContainerApps => 6(MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs:12) andDatabaseNamePrefix => "ADC_"(:14). - Depends on -
CostTagConventionTestsBase(see the per-project rollup;MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:29), and theinfra.main.bicepmanifest resource the base reads from the derived type's assembly (CostTagConventionTestsBase.cs:45,:51), the same resource ObservabilityConventionTests uses. - Concept introduced, cost as a governed dimension, authored once. Nothing in Bicep enforces a tagging convention; an untagged container app or database still deploys, it just becomes an unattributed line in the bill. The base owns both facts.
EveryContainerApp_CarriesAServiceTag(CostTagConventionTestsBase.cs:59) requires at leastMinimumContainerAppsapps to be discovered (so the check cannot pass off a parse that matched nothing; ADC's six are four services, the Gateway and the UI host) and fails any app whose captured tags line lacksservice:(:68).ServiceDatabases_CarryTheServiceTagOfTheOwningApp(:82) builds the expected literaltags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) })fromDatabaseNamePrefix(:88) and asserts the Bicep contains it, so a database's tag is derived from its own name.[Rubric §34 - Architecture Governance & Documentation]is the base's stated category (:6); the FinOps half of[Rubric §29 - Resilience & Business Continuity]applies too. - Walkthrough - two overrides and no test method. The prefix is why the base takes a parameter: ADC strips
ADC_to getconference, and an empty prefix fails the second fact (:84). - Why it's built this way - the rule lives in the shared package so Store and ADC cannot drift on it; ADC keeps only its identity (the same thin-subclass shape as BrandColorTokenTests). The trade-off is the hand-maintained floor: a new deployable raises the count and the override must follow.
- Where it's used - runs with the rest of the architecture suite in ADC's
build-and-testjob. - Caveats / not-in-source - the tag test is a substring match on the captured
tags:line, so aservice:text elsewhere on that line would satisfy it.
SqlAuditConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/SqlAuditConventionTests.cs:12· Level 0 · class
- What it is - the guard that every database carrying
dbo.AuditTrailEntriesalso provisions a database-scoped SQL auditing policy that records an UPDATE or DELETE against that table by ANY principal, the admin login included (MMCA.ADC.Architecture.Tests/Governance/SqlAuditConventionTests.cs:97-:110,:112-:129). See ADR-075. - Depends on - the same embedded
infra.main.bicepresourceCostTagConventionTestsreads, loaded independently by its ownReadBicep()(:131-:138). - Concept introduced, the audit trail must itself be tamper-evident. AuditSaveChangesInterceptorTests (elsewhere in this unit) proves the application writes an audit row for every change; this type proves the database layer under that write cannot be bypassed by a principal with direct SQL access, the admin login included, which is exactly the actor the application-level interceptor cannot see.
DatabaseAuditingPolicy_RecordsTrailUpdatesAndDeletes(:96) asserts three literal substrings: the resource type must beMicrosoft.Sql/servers/databases/auditingSettings@...(database-scoped, never the server-level policy, because object-scoped actions likeUPDATE ON dbo.AuditTrailEntriesare only valid there), and the two action groups'UPDATE ON dbo.AuditTrailEntries BY public'and'DELETE ON dbo.AuditTrailEntries BY public'.AuditTrailDatabases_IncludeEveryDatabaseThatCarriesTheTrail(:112) is a[Theory]over the three databases that carry the trail table,ADC_Identity,ADC_Conference,ADC_Engagement(:113-:115), and asserts each name appears inside theauditTrailDatabaseNamesarray the DML audit loop iterates, so a fourth module that starts writing audit rows but is never added to that array fails the build instead of shipping an unaudited trail silently.[Rubric §11 - Security]and[Rubric §30 - Compliance/Privacy/Data Governance]are the direct targets: an audit trail that can be quietly altered or deleted by a privileged account is not evidence. - Walkthrough -
AuditTrailDatabases_IncludeEveryDatabaseThatCarriesTheTraillocates thevar auditTrailDatabaseNames = [declaration by substring search (:120), asserts it was found at all (:121), then slices to the matching]and checks membership of the quoted database name inside that slice (:123-:128), so the assertion is scoped to the declared list and cannot be satisfied by the name appearing anywhere else in the file. - Why it's built this way -
BY publicis the SQL Server auditing wildcard for "every principal", written into the policy rather than into an allowlist of expected callers, because the whole point is that the admin login itself cannot make an audit-trail change disappear from the trail. - Where it's used - runs with the rest of the suite in the
build-and-testjob (MMCA.ADC/.github/workflows/deploy.yml:189,:284). - Caveats / not-in-source - the three literal substrings are exact-text assertions against the compiled Bicep, so a semantically equivalent but differently formatted policy declaration (reordered JSON keys, a different quoting style) would fail this check even though the deployed policy is correct.
ObservabilityConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7· Level 1 · class (public, sealed)
- What it is - the SLO alert-to-runbook pairing gate for ADC. It derives from
ObservabilityConventionTestsBaseand overrides one member, raising the non-vacuity floor from the base's default of 3 to 6 (MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7-:15). - Depends on - ObservabilityConventionTestsBase, plus two
EmbeddedResourceentries in the csproj that supply the files the base reads:infra/main.bicepunder the logical nameinfra.main.bicepandinfra/OPERATIONS.mdunderinfra.OPERATIONS.md(MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:17-:22). - Concept introduced, identity by inheritance plus a floor override. This is the thin-subclass pattern from BrandColorTokenTests, extended one step. The base defaults
ResourceAssemblytoGetType().Assembly(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51), so the derived type is the configuration: deriving in this assembly is what points the rule at ADC's embedded bicep and runbook, the same identity-only wiring the file comment describes ("this repo supplies only its identity",ObservabilityConventionTests.cs:3-:6). On top of that wiring, this subclass raisesMinimumAlertSpecsfrom the base's default of 3 to 6 (:14), because ADC declares six SLO alert specs, the five request and resilience SLOs plus the AI scoring token ceiling (moved intosloAlertSpecsso the pairing gate covers it), and leaving the base floor in place would let the gate pass vacuously if the parse anchors drifted and discovered only three.[Rubric section 13 - Observability & Operability]assesses whether the system can be operated under failure, which means alerts that lead somewhere; this pairs each provisioned alert with a runbook section at build time instead of at 3am. - Walkthrough - one member.
MinimumAlertSpecs => 6(:14) raises the base's default of 3 (ObservabilityConventionTestsBase.cs:39) to ADC's actual alert-spec count, now that the AI scoring token ceiling moved intosloAlertSpecsalongside the five request and resilience SLOs. Everything else runs from the base's three inherited facts:SloAlertSpecs_AreDiscovered_GateIsNotVacuous(ObservabilityConventionTestsBase.cs:82) enforces that floor;EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection(:64) walks the alerts declared in the embedded bicep and requires a matching, severity-correct section in the embedded runbook; andEveryRunbookAlertSection_MapsToAProvisionedAlert(:92) closes the other direction, failing on an orphan runbook section for an alert that no longer exists. The resource names the base reads default toinfra.main.bicepandinfra.OPERATIONS.md(:42,:45), which is why the csproj logical names must match exactly. - Why it's built this way - alert definitions live in infrastructure-as-code and the response procedure lives in a Markdown runbook; nothing in either file references the other, so the pairing is exactly the kind of invariant that decays silently. Embedding both into the test assembly turns the pairing into a compile-and-run artifact.
- Where it's used - runs with the rest of the suite in the
build-and-testjob (MMCA.ADC/.github/workflows/deploy.yml:189,:284).
BrandColorTokenTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12· Level 1 · class (public, sealed)
- What it is - the ADC end of the brand-token drift guard. It is a six-line subclass of the shared BrandColorTokenTestsBase that names one embedded stylesheet,
ADCHome.Shared.razor.css(MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:14-:17); the rule body itself lives in MMCA.Common. - Depends on - BrandColorTokenTestsBase from the
MMCA.Common.Testing.Architecturepackage (referenced atMMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:41), plus theEmbeddedResourceitem that maps the conference landing page's scoped stylesheet into this assembly under that logical name (MMCA.ADC.Architecture.Tests/MMCA.ADC.Architecture.Tests.csproj:11-:13). Externals: xUnit v3, AwesomeAssertions, and NetArchTest (MMCA.ADC.Architecture.Tests.csproj:25-:27), the last two reachable everywhere in the assembly through the global usings (MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1-:5). - Concept introduced, the thin-subclass fitness function. Every type in this unit follows one shape, so learn it once here. A fitness function is an executable test that asserts an architectural property instead of a behavior. MMCA keeps the property's logic in exactly one place, an abstract
*TestsBasein the sharedMMCA.Common.Testing.Architecturepackage, and each repo derives a sealed subclass that supplies only its own identity: which assemblies to scan, which floors and allowlists apply, which files to read. xUnit discovers[Fact]s on inherited members, so the subclass needs no test method of its own; deriving the class is what makes the rule run in this repo (ADR-015).[Rubric §34 - Architecture Governance & Documentation]assesses whether architectural decisions are recorded and enforced rather than trusted to reviewers; here the decision is enforced by a build that goes red.[Rubric §20 - Design System & Theming]assesses whether a design system has one source of truth for its tokens; this rule is what stops a host copy of the landing page from re-hardcoding the brand hex. - Walkthrough - one member.
EmbeddedCssLogicalNames(BrandColorTokenTests.cs:14-:17) is a collection expression with a single entry,"ADCHome.Shared.razor.css". That string is not a file path: it is theLogicalNamethe csproj assigns when it embedsSource/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Home/ADCHome.razor.cssas a manifest resource (MMCA.ADC.Architecture.Tests.csproj:11-:13), which is how a test assembly reads a file from a project it does not reference. The inherited factLandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:25) then loads each named resource and asserts the token rule over it; the abstract hook this class implements is declared at:22. - Why it's built this way - the class comment records the split (
BrandColorTokenTests.cs:3-:11): MMCA.Common's ownBrandColorTokenTestsguards the C#-to-CSS token definition, and this one guards the ADC consumer of it. Embedding the stylesheet rather than reading it off disk means the guard travels with the compiled test assembly and cannot be defeated by a runner whose working directory differs. - Where it's used - the whole project is inside ADC's CI solution filter (
MMCA.ADC/MMCA.ADC.CI.slnf:58), which thebuild-and-testjob restores, builds, and tests on every PR and every push tomain(MMCA.ADC/.github/workflows/deploy.yml:4-:7,:189,:264,:270,:284). - Caveats / not-in-source - the guard only covers stylesheets that are both embedded and listed. ADC lists exactly one, so a second landing-page stylesheet added later is invisible to the rule until someone adds it to both places.
MessageBusBackpressureTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs:10· Level 1 · class (public, sealed)
- What it is - the broker-backpressure gate for ADC's four service hosts. It overrides one member,
Services, as["Conference", "Identity", "Engagement", "Notification"](MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs:12). - Depends on -
MessageBusBackpressureTestsBase(see the per-project rollup;MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:31), plus one embeddedappsettings.jsonper listed service under the logical nameservices.{Service}.appsettings.json(MessageBusBackpressureTestsBase.cs:63). - Concept introduced, an unset knob is a silent default.
MessageBusSettingsleavesPrefetchCountandConcurrentMessageLimitnull and ignores values of zero or less, so a service that omits them runs on the transport's own defaults with nothing failing (MessageBusBackpressureTestsBase.cs:7-:12). The inherited factEveryServiceAppSettings_BoundsBrokerBackpressure(:70) requires a non-empty service list, then for each service both keys set and positive and the prefetch window at least the concurrency it feeds; the public staticCheck(:93) holds the parse so it can be tested over a fixture document. The settings are read withSystem.Text.Json, not bound toMessageBusSettings, because the package takes no framework reference (:24-:28).[Rubric §12]is the base's stated category (:7). - Walkthrough - one member. The list names all four services, Notification included.
- Why it's built this way - the key names are pinned against
MessageBusSettingsby MMCA.Common's own tests (:27-:28), so a rename there fails in the framework before it can desynchronize this gate. - Where it's used - runs with the architecture suite in ADC's
build-and-testjob. - Caveats / not-in-source - the gate reads the checked-in
appsettings.json; a value overridden by an environment variable in Azure is not seen.
ImageSizingTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/ImageSizingTests.cs:13· Level 4 · class (public, sealed, partial)
- What it is - a markup-to-stylesheet drift guard for two images that can overflow a phone: the public sponsor-list logo and the badge QR code. Each fact finds the element in its
.razorfile, reads itsClassattribute, and asserts some stylesheet rule underSource/that names one of those classes declares the sizing the page needs (MMCA.ADC.Architecture.Tests/Ui/ImageSizingTests.cs:19,:33). - Depends on - ArchitectureMapBase
.FindRepoRoot("MMCA.ADC.slnx")for the source root (:15),System.Text.RegularExpressionsthroughGeneratedRegexproperties with a 2000 ms timeout (:85-:109), and xUnit and AwesomeAssertions. - Concept introduced, assert the cascade without a browser.
PublicSponsorListLogo_IsCappedToItsCardWidth(:19) requires a rule withmax-width: 100%,height: autoand amax-heightcap on theMudImagebound tosponsor.LogoUrlinPublicSponsorList.razor.MyBadgeQrCode_ShrinksToThePhoneWidthAndStaysCrisp(:33) requiresmax-width: 100%,height: autoandimage-rendering: pixelatedon theQrCodeImageinMyBadge.razor, so a scaled QR stays scannable.[Rubric §34 - Architecture Governance & Documentation]covers the enforced-not-trusted half: a responsive-layout regression fails the build instead of reaching a phone. - Walkthrough - two facts and two helpers.
ClassesOf(:46) locates the element with a regex, fails if the markup moved, and splits itsClassattribute.RulesFor(:60) walks every*.cssunderSource/outsidebinandobj(global sheets and scoped.razor.cssalike), strips comments, and returns the declaration body of each rule whose selector names one of the classes, matched with a trailing(?![\w-])sologodoes not matchlogo-large. - Why it's built this way - reading CSS as text lets a plain architecture assembly check a Blazor UI it does not render. The
because:text lists the classes found, so a failure names what to fix. - Where it's used - only inside the defining file.
- Caveats / not-in-source - the rule regex excludes inner braces (
:97), so a rule nested in a media query is not understood.
LegalDocumentConsistencyTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/LegalDocumentConsistencyTests.cs:22· Level 4 · class (public, sealed, partial)
- What it is - the guard that ADC's legal surface cannot drift apart: the terms version the Identity service enforces, the two copies of the privacy policy, and the production legal links the UI hosts ship (
MMCA.ADC.Architecture.Tests/Governance/LegalDocumentConsistencyTests.cs:41,:62,:74). - Depends on - ArchitectureMapBase
.FindRepoRoot("MMCA.ADC.slnx")(:30),System.Text.Jsonfor the settings files (JsonOptionsskips comments and allows trailing commas,:24-:28), a source-generated regex for the effective date (:132), and xUnit and AwesomeAssertions. - Concept introduced, documents as executable contracts.
CurrentTermsVersion_IsPrintedVerbatimInTermsHtml(:41) readsLegal:CurrentTermsVersionfrom the Identity service'sappsettings.json, requires it non-blank (an empty value silently turns terms acceptance off), and asserts the Gateway'swwwroot/terms.htmlcontains it verbatim.PrivacyMarkdownAndPrivacyHtml_StateTheSameEffectiveDate(:62) parses theEffective date:line ofPRIVACY.mdand ofprivacy.htmlwithReadEffectiveDate(:114, formatMMMM d, yyyy) and requires them equal. The[Theory]ProductionLegalUrls_AreAbsoluteHttpsOnAPublicHost_AndPointAtTheGatewayPages(:74) runs overProductionLegalSettingsFiles(:34, the Web host'sappsettings.Production.jsonand the MAUI host'sappsettings.json) and callsAssertPublicHttpsUrl(:90) forTermsUrl(suffix/terms),PrivacyUrl(/privacy) andDeleteAccountUrl(/delete-account), plusCodeOfConductUrlwhen present: absolute, https, and not a local host (IsLocalHost,:109).[Rubric §30 - Compliance/Privacy/Data Governance]assesses whether legal commitments stay consistent with what ships. - Walkthrough - three guards, one per drift path: version against the page the user reads, markdown against html copy, and configuration against the Gateway's pages.
- Why it's built this way - the assertion messages state the maintenance rule ("when the terms change, update terms.html and bump the version together",
:55-:58), so the failure is the instruction. - Where it's used - only inside the defining file.
- Caveats / not-in-source - the version check is a substring test, so
1.1would also pass against a page that prints1.10.
MauiHostShellTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/MauiHostShellTests.cs:18· Level 4 · class (public, sealed, partial)
- What it is - four text-level guards over the MAUI head's native shell, which the architecture assembly does not reference: the Android camera intent query, the pre-theme host page background, a language-neutral loading placeholder, and the Android App Links path restrictions (
MMCA.ADC.Architecture.Tests/Ui/MauiHostShellTests.cs:26,:42,:61,:77). - Depends on - ArchitectureMapBase
.FindRepoRoot("MMCA.ADC.slnx")to locateSource/Hosts/UI/MMCA.ADC.UI(:22),System.Xml.Linqfor the manifest (theAndroidnamespace,:20), andGeneratedRegexproperties (:126-:160). It is the sibling of MobileHostParityTests, which guards the hostnames. - Concept introduced, platform requirements as tests.
AndroidManifest_DeclaresTheImageCaptureIntentQuery(:26) requires a<queries><intent><action>forandroid.media.action.IMAGE_CAPTURE, because on Android 11+ the camera app is invisible toMediaPicker.CapturePhotoAsyncwithout it and Take photo silently does nothing.HostPage_DoesNotPaintALightBackgroundBeforeTheThemeApplies(:42) joins the inline styles ofwwwroot/index.htmlwithapp.cssand requires thehtml/bodyrule to betransparent, or aprefers-color-scheme: darkblock plus a background.HostPage_LoadingPlaceholderIsLanguageNeutral(:61) takes the text between the#approot and the first<script, strips tags, and requires no letter (\p{L}), since it renders before localization exists.AndroidAppLinks_ClaimOnlyTheDeepLinkablePaths(:77) finds each[IntentFilter]mentioningPublicWebHostinMainActivity.cs, resolves itsDataPathPrefixvalues throughPathPrefixValues(:105, literals andconst stringnames), requires them non-empty, requires/conference/,/happening-now,/feedback/and/auth/oauth-completeto be claimed, and forbids/, empty and/login*.[Rubric §34 - Architecture Governance & Documentation]is the enforced-not-trusted half. - Walkthrough - four facts; the fourth is the interesting one, because an App Links filter with no path restriction claims the whole host, so
/loginwould open the app instead of the browser. - Why it's built this way - each assertion's
because:records the platform failure it prevents, the part a reader cannot infer from the attribute or CSS alone. - Where it's used - only inside the defining file.
- Caveats / not-in-source - these are regex reads of source text, not a build of the MAUI head; a differently shaped
IntentFilterdeclaration would need the regexes revisited.
UIModuleNavItems
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:460· Level 2 · record (private, sealed, nested)
- What it is - the nav items read from one ADC UI module:
Name,RelativePath(the module's source file),Items(IReadOnlyList<NavItem>) andProblems(strings for anything that could not be read) (NavItemGuardContractTests.cs:460). - Depends on -
NavItemandIUIModulefromMMCA.Common.UI; nested inNavItemGuardContractTests. - Concept introduced, two read paths, one result shape.
ReadModule(:156) instantiates the module and readsNavItemswhen its assembly is next to the test binaries (:159-:165); otherwiseReadModuleFromSource(:185) parses theNavItems { get; } = [ ... ];initializer with regexes, which is how the MAUI head'sDeviceUIModuleis covered although a net10.0 test project cannot reference it. Both return this record, andProblemscarries unreadable entries (:190,:204) soNavItems_AreReadFromEveryUIModule_GateIsNotVacuous(:55) fails instead of skipping them. - Where it's used - only inside the defining file:
DiscoverModules(:139) builds the list that the three facts iterate.
MobileHostParityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/MobileHostParityTests.cs:16· Level 4 · class (public, sealed, partial)
- What it is - the drift guard over the production hostnames the MAUI head hardcodes in four places no compiler connects:
Api:ApiEndpointandPublicSite:BaseUrlin the embeddedappsettings.json, the Android App Links hostMainActivity.PublicWebHost, and the iOSapplinks:associated-domains entitlement (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/MobileHostParityTests.cs:6-:15). Unlike the other UI rules in this unit it derives from no shared base: it is an ADC-only test with one fact. - Depends on - ArchitectureMapBase
.FindRepoRoot("MMCA.ADC.slnx")to locateSource/Hosts/UI/MMCA.ADC.UIfrom the compiled test assembly (MobileHostParityTests.cs:36-:37), and the BCLSystem.Text.JsonandSystem.Text.RegularExpressions(:1-:2). Externals: xUnit v3 and AwesomeAssertions, global-used across the assembly (MMCA.ADC.Architecture.Tests/GlobalUsings.cs:1-:5). - Concept introduced, a cross-file configuration parity check. The four values live in a JSON file, a C# constant and a plist, so a type system cannot tie them together; only a test that reads all three files can. The rule states two different relationships. The web UI serves the app-link association, so
PublicSite:BaseUrl, the Android host and the iOSapplinks:host must be the identical host. The API endpoint is the gateway, a different app, so it is only required to share the Container Apps environment domain with the web host (:10-:13).[Rubric §34 - Architecture Governance & Documentation]covers the enforced-not-trusted half: an environment move or a custom-domain cutover that misses one spot fails the build instead of shipping a mobile build whose deep links or API calls point at the old environment (:13-:14). - Walkthrough - one fact and four helpers.
PublicSiteAndAppLinkHosts_AreIdentical_AndTheApiSharesTheirEnvironment(:33-:59) parsesappsettings.jsonwithJsonOptions(comments skipped, trailing commas allowed,:27-:31) and takes theHostofApi:ApiEndpointandPublicSite:BaseUrl(:39-:43); today those are theadc-prod-gatewayandadc-prod-uihosts (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/appsettings.json:20,:23). It readsPlatforms/Android/MainActivity.csandPlatforms/iOS/Entitlements.plistas text (:45-:52), whose declarations sit atMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/Platforms/Android/MainActivity.cs:44andMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI/Platforms/iOS/Entitlements.plist:11. It then asserts Android equals the public-site host, iOS equals the public-site host, and the environment domains of the API and public-site hosts are equal, each with abecause:that names the platform requirement (:54-:58).SingleCapture(:61-:66) requires exactly one match before returning thehostgroup, so a duplicated or missing declaration fails with "exactly one declaration of {what} is expected" rather than silently comparing the first hit.EnvironmentDomain(:68-:69) strips everything up to the first dot, turningadc-prod-ui.<env>.eastus2.azurecontainerapps.iointo<env>.eastus2.azurecontainerapps.io.AndroidPublicWebHostandIosAppLinksHost(:71-:75) areGeneratedRegexproperties withExplicitCaptureand a 2000 ms match timeout, matchingPublicWebHost = "..."and<string>applinks:...</string>respectively.
- Why it's built this way - reading the platform files as text rather than compiling them is what lets an architecture test assembly check a MAUI head it does not reference (the MAUI head is not in ADC's CI solution filter, which lists this test project at
MMCA.ADC/MMCA.ADC.CI.slnf:59). TheMainActivityconstant carries its own comment pointing back atPublicSite:BaseUrl(MainActivity.cs:37), and this test is what makes that comment binding. - Where it's used - no usage outside the defining file; it runs with the rest of the architecture suite in the
build-and-testjob (MMCA.ADC/.github/workflows/deploy.yml:220), whose test step runsdotnet test --solution MMCA.ADC.CI.slnf(:346). - Caveats / not-in-source - the environment check compares everything after the first label, so it assumes both hosts are single-label app names under the same environment suffix; a custom domain that is not shaped
<app>.<environment domain>would need the rule revisited. Not determinable from source: whether an AndroidAndroidManifestintent filter or other platform files carry the host as well (the test reads only the three files named above).
NavigationContractTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/NavigationContractTests.cs:20· Level 4 · class (public, sealed, partial)
- What it is - ADC's navigation drift gate (rubric §25): the
## Route Contracttable inadc-NavigationFlow.mdmust list exactly the routable pages ADC ships, each row's Access word must match the page's guard, and every page must state its guard as exactly one recognized attribute. It is the ADC counterpart of the framework's own same-named gate inMMCA.Common.Architecture.Tests.Ui, a different class. - Depends on - ArchitectureMapBase
.FindRepoRoot("MMCA.ADC.slnx")(NavigationContractTests.cs:28), the nested records PageRoute, ContractRow and ContractTable (:328-:332),GeneratedRegexproperties (:296-:325), and xUnit and AwesomeAssertions. Unlike the framework gate it reflects over nothing: it scans*.razorsource text. - Concept introduced, documentation as an executable contract, read from source text. The doc lives in the Website repo, not next to the code, so
ResolveDocPath(:288) reads it from theADC_NAVIGATION_DOCenvironment variable (DocVariable,:23) and otherwise from../Website/docs-src/guides/adc-NavigationFlow.md(DefaultDocPath,:30-:31); the existence assertion inReadContractTable(:243) says CI downloads the published copy and sets the variable.[Rubric §25 - Navigation & IA]is the target;[Rubric §11 - Security]and[Rubric §26 - Front-End Security]apply because the guard column is verified against[AllowAnonymous],[Authorize]and[Authorize(Roles = RoleNames.Organizer)]. - Walkthrough - a floor
MinimumRoutes = 55(:22) and five facts.RoutablePages_AreDiscovered_GateIsNotVacuous(:38) requires at least 55 discovered routes (59 when written), so a moved tree cannot let the gate pass over nothing.EveryRoutablePage_DeclaresARecognizedAccessAttribute(:44) reports pages whoseClassifyproduced a violation (none, several, or an unrecognized attribute,:207) and any route declared by more than one page.EveryRoutablePage_IsInTheRouteContract_AndEveryContractRoute_Exists(:63) compares route sets both ways,undocumentedandphantom, verbatim including constraints such as{Id:int}.EveryContractAccess_MatchesThePageGuard(:79) compares each row's Access with the page's classified guard, skipping rows the other facts already report.RouteContractTable_IsWellFormed(:107) surfacesProblems, duplicate route rows, Access words outside Anonymous, Authenticated and Organizer, and Page cells that are neither-nor the declaring file's name.- Discovery is
DiscoverPageRoutes(:139,internal staticsoNavItemGuardContractTestsreuses it to read ADC's pages,:332): every*.razorunderSource/outsidebinandobj(IsSourceFile,:170) with an@pageline; access comes from@attributelines, else from attribute lines above the matching partial class in the.razor.cs(ReadCodeBehindAccessAttributes,:180).
- Why it's built this way - the failure messages name the remedy and its order ("landing that change before or with the code change"), so a missing row is a documentation task, not a mystery.
- Where it's used - runs in ADC's architecture suite; a checkout without the Website repo and without
ADC_NAVIGATION_DOCfails atReadContractTable's existence assertion (:243). - Caveats / not-in-source - the code-behind lookup scans attribute lines directly above the first
partial classmatching the page name; an attribute placed elsewhere reads as no guard.
SpanishAccentTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:9· Level 4 · class (public, sealed)
- What it is - the Spanish-localization gate for ADC: no
*.es.resxstring may ship a common word with its accent or n-tilde missing. It overrides one member,ResourceRoot, asSource/under the repo root found byArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx")(MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:11-:12). - Depends on -
SpanishAccentTestsBase(see the per-project rollup;MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:27) and ArchitectureMapBase. - Concept introduced, a localization defect that every functional test misses. An unaccented word compiles, renders and passes behavior tests, so a Spanish-speaking user finds it. The base's single fact
Spanish_resources_keep_their_accents(SpanishAccentTestsBase.cs:68) enumerates*.es.resxunder the root outside build output, requires at leastMinimumResourceFileCount(default 1,:64) so a wrong root cannot pass vacuously, builds one whole-word case-insensitive regex fromUnaccentedWords(default list:32, extendable per consumer,:51), and checks only string<value>text, not keys or comments.AllowedEntries(:58) holds deliberate exceptions as{path}:{key}. ADC overrides neither, so it takes the framework list and no exceptions.[Rubric §34 - Architecture Governance & Documentation]covers the enforced-not-trusted half. - Walkthrough - one override; the rule is inherited.
- Why it's built this way - the word list leaves out words correct both ways (
esta,solo) and plurals that drop the accent by rule (SpanishAccentTestsBase.cs:20-:23), trading recall for a gate with no false positives. - Where it's used - runs with the architecture suite in ADC's
build-and-testjob; the framework's ownSpanishAccentTestssubclasses the same base. - Caveats / not-in-source - only the listed words are caught; a missing accent on any other word passes.
NavItemGuardContractTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:41· Level 5 · class (public, sealed, partial)
- What it is - the menu-to-page guard contract: every sidebar entry an ADC UI module contributes must be no looser than the guard of the page it opens, so the menu never offers a link the page will refuse. Three facts enforce it: the nav items were actually read, every item opens a routable page, and every item requires at least what that page's guard enforces (
MMCA.ADC.Architecture.Tests/Api/NavItemGuardContractTests.cs:54-:106). The contract is written out in the class summary (:16-:40). - Depends on - IUIModule and NavItem (with NavSection) for the menu side, NavigationContractTests for ADC's own page routes and access classification (
NavigationContractTests.DiscoverPageRoutes, called at:318), RoleNames for theOrganizerrole, and ArchitectureMapBase forFindRepoRoot("MMCA.ADC.slnx")(:45). It takes no architecture map. - Concept introduced, comparing two guards that are written in different places. A navigation entry and the page behind it each carry an access decision, declared independently, so nothing in a single file notices when the menu shows a signed-out visitor a link that ends in a sign-in challenge. The shared NavMenu shows an item to a caller who satisfies its
RequiredRole,RequiredClaimandRequiredPermission(each only when set), and shows the User and Admin sections to signed-in callers only, so an item counts as anonymous exactly when it sets none of the three and sits inNavSection.General(:116).Compare(:108-:136) then yields a problem per rule: an authenticated page needs a non-anonymous item, a page naming roles needs an item requiring one of them (no permission or claim implies a role statically, because the role-to-permission map is editable at runtime), and a page requiring a claim or a permission needs an item carrying the same one.[Rubric §25]is the section the assertion messages cite (:71,:85,:105);[Rubric §11 - Security]and[Rubric §26 - Front-End Security]apply because the pages are the authorization boundary and the menu must not contradict it. - Walkthrough - three facts, then the readers behind them.
NavItems_AreReadFromEveryUIModule_GateIsNotVacuous(:54-:72) fails when noIUIModuleimplementation is found underSource/, when a module reports a read problem, or when zero items were read in total, so the gate cannot pass by reading nothing.EveryNavItem_OpensARoutablePage(:74-:86) fails on a dead link: an item whoseHrefmatches no@pageroute, which would also make the guard comparison pass without checking anything.EveryNavItem_IsNoLooserThanTheGuardOfThePageItOpens(:88-:106) runsComparefor each item against every page itsHrefresolves to. The failure message says to fix the nav item (add the role, claim or permission, or move it to the User or Admin section), never to loosen the page guard.DiscoverModules(:139-:154) finds module classes in source withModuleDeclarationRegex(:442), skippingbinandobj(:435-:440).ReadModule(:156-:169) uses the real instance (Activator.CreateInstance,.NavItems) when the owning project's assembly sits beside the test binary (FindOwningAssemblyName,:171-:183), and otherwiseReadModuleFromSource(:185-:209). That fallback exists for the MAUI head'sDeviceUIModule, which a net10.0 test project cannot reference (:34-:39). It parses theNavItems { get; } = [ ... ];initializer with generated regexes (:442-:458), splits arguments depth-aware (SplitArguments,:283-:313) and resolves only string literals,null,RoleNames.*andNavSection.*(TryResolveValue,:250-:281). An entry it cannot resolve is reported as a problem, so it fails the first fact instead of being skipped (ParseEntry,:211-:245).DiscoverGuardedPages(:316-:337) builds the page side two ways. ADC's own routable components come fromNavigationContractTests, whose access string (Anonymous,Authenticated,Organizer) is mapped to aPageGuard(:318-:326). The framework's routable components inMMCA.Common.UIare read by reflection:ReadComponentGuard(:350-:404) collects[Authorize]roles and, for each named policy, walks the policy requirements. It understands deny-anonymous, roles, a single-value permission claim, any other claim type, andNotificationPageRequirementby type name (:376-:399).RouteMatches(:407-:433) compares segment by segment, case-insensitively, treating{param}as a wildcard,{*catchall}as matching the rest, and trailing{x?}as optional; the query string and fragment are stripped first (:409).
- Why it's built this way - policies are read from the real registration rather than restated:
ReadFrameworkAuthorizationOptionsbuilds aServiceCollection, callsAddUIShared, and pullsAuthorizationOptionsfrom the provider (:339-:348), so the gate follows the framework's actual policy. An unrecognized requirement type returns a problem naming itself ("teach NavItemGuardContractTests what it enforces",:397) instead of silently passing. The unresolved cases always fail loudly: that is the design rule that keeps a contract test from going vacuous. - Where it's used - runs with the architecture suite; the nav items it reads come from the Identity, Conference, Engagement and device UI modules, for example
MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.UI/IdentityUIModule.cs. - Caveats / not-in-source - the source reader understands only literal forms; a nav item built from a constant or a helper must be rewritten in literal form or its module referenced by the test project. The comparison is static on roles, so it cannot prove a permission-gated item reaches a role-gated page.
AnonymousEndpointTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:23· Level 6 · class (public, sealed)
- What it is - the security gate that no endpoint loses its authorization unnoticed: every
[AllowAnonymous]reachable in ADC's three module API assemblies and three module UI assemblies must appear as a reviewed line in this file (MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:25-:33,:35-:131). It is the largest subclass in the unit: 60 allowlist entries covering controllers and routable pages alike. - Depends on - AnonymousEndpointTestsBase,
System.Reflection.Assembly(global-used atMMCA.ADC.Architecture.Tests/GlobalUsings.cs:1), and, as type references pinning the three API assemblies, IdentityModule, ConferenceModule, and EngagementModule (:27-:29). The three UI assemblies are loaded by name (:30-:32). Note it does not take the map: it names its own assembly set. - Concept introduced, the reviewed-allowlist gate. The rules elsewhere in this unit assert a structural property. This one asserts a review property: the set of anonymous endpoints is not wrong, it is simply not allowed to change silently. The base makes that stick in three directions at once:
AnonymousEndpoints_AreAllowListedfails on an unlisted[AllowAnonymous](MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:80),ScannedEndpointSet_IsNotEmptyfails when fewer thanMinimumScannedTypesendpoint types were discovered at all (:92), andAllowList_HasNoStaleEntriesfails on a listed entry that no longer matches anything (:105), which is what stops the list from silently accumulating names for endpoints that were renamed or re-gated.[Rubric §11 - Security]assesses whether the authorization posture is deliberate and verifiable; an allowlist that must be edited, in a file a reviewer reads, is the mechanism.[Rubric §26 - Front-End Security]applies too, because routable Blazor components are scanned alongside controllers. - Walkthrough - four members.
TargetAssemblies(:25-:33) names six assemblies: the Identity, Conference, and Engagement API assemblies by anchor type, and the matching three UI assemblies viaAssembly.Load. The base scans each for the two shapes it understands, MVC controllers (any type whose base chain reachesControllerBase,AnonymousEndpointTestsBase.cs:156-:170) and routable Blazor components (any type carrying aRouteAttribute,:172-:176), both matched by attribute full name so the rule library keeps no ASP.NET reference (:34-:35).AllowedAnonymousEndpoints(:35-:131) is the reviewed list, grouped by justification rather than alphabetically. Two Identity credential-exchange actions on AuthController,LoginAsyncandRegisterAsync, because requiring a token to mint one would be circular; they are throttled by the auth-ip rate-limit policy instead (:37-:41). Then the Conference public-browse reads: theGetAllAsync/GetAllForLookupAsync/GetByIdAsynctriple on fourteen agenda controllers (ActivitiesController, CategoryItemsController, EventsController, PartnersController, QuestionsController, RoomsController, SessionsController, SpeakersController, SponsorsController, and their category and join siblings,:47-:88), because the conference website is readable without an account and is output-cached per ADR-040; the comment is careful to note that every create/update/delete on those same controllers stays behind the class-level[HasPermission](:43-:46). Then the session-materials read onSessionAssetsController.GetBySessionAsync(:95), part of the public session page and output-cached like the agenda reads above, scoped by the same public-session projection so a session the public cannot see answers an EMPTY list rather than leaking one; every write on that controller stays behind[Authorize]and is re-authorized in the handler against the session's own speaker list (:90-:94). Then three smaller families with their own reasons: the Now/Next wayfinding reads (:97-:100), the ICS calendar exports a calendar client fetches without a bearer token (:102-:105), and the aggregate bookmark counts behind the popularity badge, counts only and never a per-user list (:107-:110). Then the type-level entry for ServiceInfoController, which must answer before a caller has a token to negotiate with (:114). Last, the eight public conference-website pages (PublicActivityList,PublicEventDetail,PublicEventList,PublicSessionDetail,PublicSessionList,PublicSpeakerDetail,PublicSpeakerList,PublicSponsorList,:123-:130) that render the agenda, speakers, sponsors and activities a visitor browses before creating an account. Type-level and method-level attributes use different identifier shapes, a bareFullNameversusFullName.MethodName, which is why theServiceInfoControllerentry and the page entries carry no method suffix.RequireExplicitAuthorizationDecision => true(:139) is SEC-ADC-03: an endpoint carrying neither[Authorize]nor[AllowAnonymous]is invisible to the allowlist check above, exactly the shape a forgotten[Authorize]produces. The base now defaults the same way (AnonymousEndpointTestsBase.cs:69, backed byEndpoints_DeclareAnAuthorizationDecisionat:119), so the ADC override restates the default explicitly. On a controller the framework's fallback authorization policy also closes the gap at run time; on a routable page nothing does, becauseAuthorizeRouteViewreads attributes and ignores the fallback entirely, so for pages this flag is the only control (AnonymousEndpointTests.cs:133-:138).MinimumScannedTypes => 80(:144) raises the base floor of 1 (AnonymousEndpointTestsBase.cs:52) to the exact count of controller and routable-component types across the six assemblies today,SessionAssetsControllerincluded, so a renamed assembly or a dropped surface is a failure rather than a quietly smaller scan (:141-:143).
- Why it's built this way - the class comment explains what the absences mean, which is the part a reader cannot infer. The two password-recovery actions on PasswordResetController and the two email-confirmation actions on
EmailConfirmationController(ADR-116) are anonymous for the same circularity reason as login, but ADC does not override them, so the framework bases own their allowlist entries and none appear here (:11-:16); the same is true of refresh (:39). The base reads attributes withDeclaredOnlyandinherit: falseprecisely so an inherited framework action is reported once at its declaration site rather than once per derived controller in every consumer (AnonymousEndpointTestsBase.cs:262-:264). Notification is absent because that module ships no controller and no routable component, hosting only the SignalR hub, so it contributes nothing to the scan (:18-:21). - Caveats / not-in-source - the base states its own blind spot: minimal-API endpoints opt out through the
.AllowAnonymous()builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own small anonymous minimal-API surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is not covered here (AnonymousEndpointTestsBase.cs:18-:24). Nothing recomputes the 80 floor, so it is a lower bound a human maintains.
InlineStyleTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10· Level 6 · class (public, sealed)
- What it is - the gate that no
.razorfile underSource/carries an inlinestyle=/Style=/...StyleFunc=attribute, so every visual rule lives in a stylesheet (adc-ui.cssor a component's scoped.razor.css) instead of scattered inline across markup. ADC's class is now a thin subclass of the framework's shared base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10-:16). - Depends on - InlineStyleTestsBase (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12) and ArchitectureMapBase.FindRepoRootto locate the repo root from the compiled test assembly (InlineStyleTests.cs:12). - Concept introduced, a text scan authored once and re-run per repo. The other UI fitness functions in this unit (
BrandColorTokenTests,SortableColumnConventionTests) read a compiled artifact; this one reads source.razorfiles, because inline styles are a markup-authoring concern that never survives into a compiled component in a form worth inspecting. The scan itself lives inArchitectureRules.RazorMarkupCarriesNoInlineStyles(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37), which the base's single factRazorMarkup_CarriesNoInlineStylescalls (InlineStyleTestsBase.cs:30-:31) with the subclass'sMinimumRazorFilesfloor. The floor is 80 (InlineStyleTests.cs:15, "enough to prove the scan actually walked the UI tree, three module UI projects and both heads"), so a moved or renamedSource/root fails loudly rather than passing on zero files scanned.[Rubric §20 - Design System & Theming]is the direct target: a single source of truth for styling is what an inlinestyle=attribute bypasses outright, and[Rubric §34 - Architecture Governance & Documentation]covers the enforced-not-trusted half. - Walkthrough - two overrides and one inherited fact.
RepoRoot(InlineStyleTests.cs:12) isArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx"), implementing the base's abstract root (InlineStyleTestsBase.cs:14-:15), andMinimumRazorFiles => 80(:15) implements the abstract floor (InlineStyleTestsBase.cs:18). The inheritedRazorMarkup_CarriesNoInlineStyles(InlineStyleTestsBase.cs:30) passesMarkupRootsandAllowedInlineStylesto the rule; ADC adds no allow-list, as the class summary states ("No allow-list: a genuinely dynamic value belongs in a class toggled from code or a CSS custom property",InlineStyleTests.cs:3-:8). - Why it's built this way - a component-parameter convention (
Style="..."on a MudBlazor component, say) is textually indistinguishable from the native HTMLstyle=attribute, so the rule catches both the same way a reviewer scanning markup would, rather than special-casing every UI library's parameter names. Moving the regex and file walk into the shared base means MMCA.Common and ADC run one implementation and differ only in root and floor. - Where it's used - runs with the rest of the suite in the
build-and-testjob (MMCA.ADC/.github/workflows/deploy.yml:189,:284). - Caveats / not-in-source - the exact regex and its lookbehind now live in the shared rule, not in this class; the rule file was not re-read for this section beyond its signature, so its false-positive heuristics are Not determinable from this unit's source.
LifetimeTokenConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10· Level 6 · class (public, sealed)
- What it is - the component-lifetime gate for ADC's module UI code: no component under
Source/Modulesreads_cts.Tokendirectly (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10-:17). - Depends on - LifetimeTokenConventionTestsBase (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13) and ArchitectureMapBase.FindRepoRoot(LifetimeTokenConventionTests.cs:13). - Concept introduced, reading a cancellation token off a disposed source. Reading
Tokenoff a disposedCancellationTokenSourcethrowsObjectDisposedExceptionand crashes the Blazor circuit after a navigation away, so components must read it as_cts.LifetimeToken()(MMCA.Common.UI) or through aLifetimeTokenproperty (LifetimeTokenConventionTestsBase.cs:6-:9). The ruleArchitectureRules.ComponentsReadTheirTokenThroughLifetimeToken(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LifetimeTokens.cs:28) does the scan; the base's single factComponents_ReadTheirTokenOnlyThroughLifetimeToken(LifetimeTokenConventionTestsBase.cs:28-:29) calls it with the subclass's scan root and file floor.[Rubric §34 - Architecture Governance & Documentation]covers the enforced-not-trusted half. - Walkthrough - two overrides and one inherited fact.
ScanRoot(LifetimeTokenConventionTests.cs:12-:13) isSource/Modulesunder the repo root found viaFindRepoRoot("MMCA.ADC.slnx"), andMinimumCodeBehindFiles => 60(:16, "enough to prove the scan actually walked the module UI code-behind, three module UI projects") is the vacuity floor, so a moved root cannot pass on zero files (LifetimeTokenConventionTestsBase.cs:18-:19). The base's optionalScanAllCodeFiles(:25) stays at its defaultfalse, so only.razorand.razor.csfiles are scanned (:21-:25). - Why it's built this way - authored once in the framework and re-run as a thin subclass in each repo, which supplies only the root and the floor (
LifetimeTokenConventionTestsBase.cs:9-:11); MMCA.Common has its own subclass of the same base. - Caveats / not-in-source - the rule's matching details are not re-read here, so how it treats a token read through a helper other than
LifetimeTokenis Not determinable from this unit's source.
ProtoContractTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3· Level 6 · class (public, sealed)
- What it is - the frozen wire contract for ADC's synchronous cross-service API. It names the eight
.protofiles the four*.Contractsprojects compile and commits a 99-line snapshot of everything they declare, so a renumbered field or a renamed rpc fails the build (MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:9-:19,:21-:122). - Depends on - ProtoContractTestsBase. Nothing else: the rule reads
.protofiles off disk from the repo root, so this class takes no map and the csproj needs no reference to the*.Contractsprojects. - Concept introduced, pinning a contract that no compiler checks. A
.protofile is a published contract between processes that are built separately, so nothing in a single repo's build notices when it changes incompatibly. The rule library rebuilds the live contract by parsing the files and diffs it against the committed list, reporting each side separately as "present in the .proto files but NOT frozen" and "frozen but NOT present in the .proto files" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:68-:77). What gets pinned is exactly the wire surface: the package, every rpc with its request/response types and streaming flags, every message field with its declared type, label, and field number, and every enum value with its number (ArchitectureRules.Protos.cs:22-:24). What is deliberately not pinned issyntax,import, andoptionlines includingcsharp_namespace, because none of them changes a byte on the wire and failing on them is the fastest way to teach a team to update a snapshot without reading it (:27-:31).[Rubric §9 - API & Contract Design]assesses contract governance across the whole surface, not just REST.[Rubric §7 - Microservices Readiness]: this list is the synchronous coupling between ADC's four services, in the same way IntegrationEventContractTests is the asynchronous one (ADR-007). - Walkthrough - three members, all implementing abstract hooks.
SolutionFileName => "MMCA.ADC.slnx"(:5) implementsProtoContractTestsBase.SolutionFileName(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22). The rule resolves the repo root from it viaArchitectureMapBase.FindRepoRoot(ArchitectureRules.Protos.cs:46), so the files are read from the working tree regardless of the runner's working directory.ProtoFiles(:9-:19) lists eight repo-root-relative paths, and the comment states the scope rule: every.protocompiled by the four*.Contractsprojects (:7-:8). Three from Conference (event_live_validation,session_bookmark_validation,user_conference_export), two from Engagement (bookmark_count,user_engagement_export), one from Identity (attendee_query), and two from Notification (live_channel,user_notification_export).FrozenProtoContracts(:21-:122) is the snapshot: 85message ...lines (:23-:107), one per field, each ending in= <number> : <type>, and 14service ...lines (:108-:121), one per rpc. The entries are sorted, which is what makes a regenerated snapshot diff line by line, and the base's<remarks>explains how to regenerate it (printArchitectureRules.BuildProtoContract(...)and paste,ProtoContractTestsBase.cs:12-:17). Reading the list is the fastest way to see what ADC's services actually say to each other: live-window validation, current-room lookup, sponsor live info and the overlapping-session lookup from Conference (GetOverlappingSessionIds,:110), plus its per-user event and session answer export (UserConferenceExportService,:23-:30,:58-:60,:115), bookmark counts and the engagement export from Engagement (the export response now also carriespoll_votes = 7andquestion_upvotes = 8, with their ownEngagementPollVoteExportItemandEngagementQuestionUpvoteExportItemmessages,ProtoContractTests.cs:91-:92,:73-:77), the attendee user-id list from Identity, and channel push plus the notification export from Notification.- The single inherited fact is
ProtoContracts_ShouldMatch_TheFrozenSnapshot(ProtoContractTestsBase.cs:33).
- Why it's built this way - the base is explicit that this is consumer-facing only: MMCA.Common ships the gRPC plumbing but no
.protoof its own, so the framework does not subclass it, and a repo with a*.Contractsproject does (ProtoContractTestsBase.cs:9-:11). The four*.Contractsprojects are not registered in AdcArchitectureMap at all: this rule works from file paths rather than mapped assemblies, which is why it reaches them anyway. - Caveats / not-in-source - the file list is hand-maintained, so a brand-new
.protoadded to a*.Contractsproject is not pinned until someone lists it here. Nothing asserts thatProtoFilescovers every.protoin the tree.
SortableColumnConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11· Level 6 · class (public, sealed)
- What it is - the MudDataGrid sorting guard: a column marked sortable must be a
PropertyColumn, not aTemplateColumn. The subclass supplies one scan root, the whole ADCSourcetree (MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:14-:17). - Depends on - SortableColumnConventionTestsBase, and ArchitectureMapBase called statically for the repo root (
:16). Like ProtoContractTests and TranslationCompletenessTests, it takes no map: the rule is a markup text scan, so a directory is all it needs. - Concept introduced, guarding a defect that renders correctly. Server-side sort reads the bound property off the column, and a
TemplateColumnhas none, soSortable="true"on one produces a header that toggles a sort arrow without ordering the data (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:4-:8). The defect compiles, renders, and stays invisible until somebody checks the order, which is the exact profile of a bug a fitness function should own rather than a code review.[Rubric §24 - Forms/Validation/UX Safety]is the base's own stated target, since an affordance that lies to the user is a UX-safety failure;[Rubric §18 - UI Architecture]and[Rubric §12 - Performance & Scalability]are the neighbours, because the whole point of server-side sort is to keep the grid from pulling the full set into the browser. - Walkthrough - one member.
MarkupRoots(:14-:17) is a single-element collection expression:Path.Combine(ArchitectureMapBase.FindRepoRoot("MMCA.ADC.slnx"), "Source"). Building the path fromFindRepoRootrather than a relative path is what makes the scan independent of the runner's working directory (SortableColumnConventionTestsBase.cs:19-:23), and pointing it at the wholeSourcetree rather than one project is what makes it cover the three module UI projects and both web heads, which the class comment states explicitly (SortableColumnConventionTests.cs:7-:9). The inherited fact isSortableColumns_ShouldNotBe_TemplateColumns(SortableColumnConventionTestsBase.cs:27), delegating toArchitectureRules.SortableGridColumnsUsePropertyColumn(MarkupRoots). The base notes that@* *@comments are ignored and that a root which does not exist fails the test rather than silently passing (:12-:13). - Why it's built this way - the class comment records that ADC already used
PropertyColumnfor every sortable column when the rule was adopted, so this subclass freezes an already-clean state rather than pinning debt (SortableColumnConventionTests.cs:7-:8). Compare RawQueryableConventionTests, which had to be adopted the other way, over an existing set of violations. - Caveats / not-in-source - detection is textual over
.razormarkup, so a grid composed at runtime, or a column whoseSortablevalue comes from a variable rather than a literal, is outside what a text scan can judge.
TranslationCompletenessTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12· Level 6 · class (public, sealed)
- What it is - the internationalization completeness gate: every base
*.resxunderSource/must have a complete, non-empty Spanish.es.resxsibling, so adding an English key without its translation fails CI instead of shipping a half-translated UI (MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:3-:11). - Depends on - LocalizationResourceTestsBase. Note the deliberate name divergence: the ADC subclass is named for what it guarantees (translation completeness), not for the base it derives from.
- Concept introduced, the non-vacuity floor. A convention scan that discovers nothing passes trivially, which is the failure mode that makes fitness functions untrustworthy over time. The MMCA bases answer it with a minimum-count floor that the subclass raises to the repo's real magnitude, so a broken scan root (a moved directory, a renamed convention, a case-sensitivity slip on the Ubuntu runner) fails loudly instead of going green while checking zero files. You have already seen the floor in AnonymousEndpointTests, and you will see it again in CommandValidatorCoverageTests, ErrorCatalogTests, FormsConventionTests, and LocalizedTextConventionTests.
[Rubric §27 - i18n]assesses whether localization is enforced rather than aspirational; the gate is the enforcement, and ADR-027 (which supersedes the single-locale ADR-011) is the decision it executes. - Walkthrough - two members.
RequiredCultures => ["es"](TranslationCompletenessTests.cs:14) implements the base's abstract culture list (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:13), so Spanish is the one culture ADC contractually completes.MinimumBaseResources => 40(TranslationCompletenessTests.cs:16) raises the base's default of 0 (LocalizationResourceTestsBase.cs:21), which would otherwise let an empty scan pass. The inherited fact isTranslations_AreComplete_ForEveryRequiredCulture(LocalizationResourceTestsBase.cs:24). - Why it's built this way - the class comment justifies the floor from the repo's real shape: ADC has 40 or more localized resource sets across the three module UIs, the UI hosts' landing page, the nav-item module descriptors, and the API error-resource sets, so a near-zero discovery count means the scan path is wrong (
TranslationCompletenessTests.cs:8-:10). - Caveats / not-in-source - the floor is a lower bound stated in the subclass, not a count computed from the tree, so it stays correct only as long as someone raises it when the resource set grows materially.
FolderWidthTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10· Level 6 · class (public, sealed)
- What it is - the layout gate: no folder under ADC's
Source/orTests/tree may hold more than 12 direct code files, so a folder keeps naming a feature instead of turning into a technical bucket. It is a four-line subclass whose only override supplies the repository root (MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:13). - Depends on - FolderWidthTestsBase and ArchitectureMapBase, whose static
FindRepoRoot("MMCA.ADC.slnx")walks up from the test assembly until it finds the solution file, which is how the rule locates the working tree regardless of the runner's working directory (FolderWidthTests.cs:12). - Concept introduced, a fitness function that reads the filesystem rather than IL. Every other rule in this unit reflects over compiled assemblies. This one cannot: the defect is a layout defect, so the rule walks directories from disk (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:9-:11,:42-:63). That is also why the subclass supplies a path instead of a map.[Rubric §5 - Modular Monolith & Vertical Slices]assesses whether the code is organized by feature rather than by technical layer; this makes "module by project, feature by folder, use case by leaf" an assertion instead of a review habit (ADR-109). - Walkthrough - one member.
RepoRoot(:12) is the single override. Everything else is inherited: the capMaxDirectFiles => 12(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), an emptyExemptFolderSuffixesdefault that ADC does not override, so ADC carries no documented exemption (:29), and the single factFolders_stay_narrow, which hands both to the rule (:31-:33). The counting rules are the interesting part: only files directly in the directory count, a.razorcomponent and its co-locatedX.razor.cscode-behind count as one unit,.resxsatellites never count, and generated files (*.g.cs,*.generated.cs,*.Designer.cs) never count because nobody chose to put them there (ArchitectureRules.FolderWidth.cs:13-:19). Directories with a path segment namedbin,obj,Migrations,Platforms,Resources,node_modules,wwwrootor.gitare skipped outright, being build output or tool-owned trees whose shape is not the author's decision (:22-:24). Violations are collected, sorted ordinally, and reported together through ArchitectureAssert.NoViolationswith the remedy in the message, "split it by feature or aggregate" (:66-:70). - Why it's built this way - the class comment names the layouts the rule protects,
Services/{Feature}/besidePages/{Feature}/,Controllers/{Aggregate}/,EntityConfiguration/{Aggregate}/, and states the failure mode it prevents: drifting back into flat technical buckets (FolderWidthTests.cs:3-:8). A cap enforced per repo rather than per project is deliberate, because the drift shows up wherever a folder is convenient, not only in the module the change belongs to. - Where it's used - the project is inside ADC's CI solution filter (
MMCA.ADC/MMCA.ADC.CI.slnf:59), which thebuild-and-testjob builds and tests on every PR (MMCA.ADC/.github/workflows/deploy.yml:205,:284,:300). - Caveats / not-in-source - the rule sees files, not namespaces, so a folder that stays under 12 while mixing unrelated features passes; the cap is a ceiling on width, not a check that the folder names one thing.
DecoratorPipelineOrderTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:29· Level 9 · class (public, sealed)
- What it is - the one type in this unit that builds a real DI container instead of reading metadata. It asserts that ADC's genuine registration sequence produces the ADR-014 decorator nesting at runtime, exercised against a real Identity command/query pair (
MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:19-:28). - Depends on - DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult> from the
MMCA.Common.Testingpackage (MMCA.ADC.Architecture.Tests.csproj:43), closed over ChangePreferencesCommand / Result and GetUserPreferencesQuery /Result<UserPreferencesResponse>(DecoratorPipelineOrderTests.cs:30). Externals:Microsoft.Extensions.DependencyInjection,Microsoft.FeatureManagement,NullLogger<>, and Moq (:1-:13). - Concept introduced, an object-graph assertion. Scrutor's
TryDecorateapplies decorators in reverse registration order, so the last decorator registered becomes the outermost wrapper. That makes an innocent-looking reorder of theAddApplicationDecorators()lines, or a module handler scan that runs after it instead of before, a silent change in runtime behavior: the code still compiles, the container still resolves, and the pipeline quietly runs validation after the transaction opens. The base turns that into a test failure by resolving the handler and walking the constructed graph via reflection over each decorator's private inner-handler field, so it verifies the objects that actually exist rather than the registration list (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:29-:32,:100-:125).[Rubric §2 - Design Patterns]assesses whether patterns are applied deliberately and correctly; the decorator chain is the framework's central pattern and this is the only test that proves its composition.[Rubric §14 - Testability]: the fact that a production registration sequence can be replayed in a bareServiceCollectionwith seven mocked dependencies is itself the evidence that the composition root is not entangled with hosting. - Walkthrough - two members.
ConfigureServices(IServiceCollection)(DecoratorPipelineOrderTests.cs:32) implements the base's single abstract hook (DecoratorPipelineOrderTestsBase.cs:46) and reads in two halves. Test doubles for the decorator constructor dependencies (:33-:39):Mock.Of<IFeatureManager>(),Mock.Of<ICurrentUserService>(),Mock.Of<IPermissionRegistry>(),Mock.Of<ICorrelationContext>(), andMock.Of<ICacheService>()as singletons, a scopedIUnitOfWorkfactory, and the open genericILogger<>mapped toNullLogger<>. These exist only so the decorators can be constructed; the test never invokes a handler. The base names the same seven dependencies as the contract for a subclass (DecoratorPipelineOrderTestsBase.cs:22-:25). The real registration sequence (:41-:42) is now one composed call,services.AddMmcaApplicationPipeline(pipeline => pipeline.ScanModule<MMCA.ADC.Identity.Application.ClassReference>()), written this way rather than the three hand-sequenced calls (AddApplication(), the module scan,AddApplicationDecorators()) so this fitness function exercises the same helper the four service hosts compose with: a regression in the helper's own ordering surfaces here as a wrong nesting rather than only as a startup failure nobody runs.AddMmcaApplicationPipelinestill runsAddApplication()first, then the module's handler scan, thenAddApplicationDecorators()last (Scrutor'sTryDecoratecan only wrap handlers already registered), and seals the collection.ComposedPipeline_LeavesNoHandlerUndecorated(:45-:56) is a[Fact]of its own, not inherited: it builds a freshServiceCollection, callsConfigureServiceson it, then assertsservices.VerifyDecoratorPipeline(). The base's nesting facts (below) prove ONE command and ONE query are wrapped correctly; this proves no handler in the whole Identity module escaped the pipeline, the failure mode that ships silently because an undecorated handler runs with no feature gate, authorization, validation or transaction and nothing throws at startup to say so.- The two inherited facts then assert the chains.
CommandPipeline_NestsDecorators_InAdr014Order(DecoratorPipelineOrderTestsBase.cs:72) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, then the concrete handler (:49-:58);QueryPipeline_NestsDecorators_InAdr014Order(:76) expects FeatureGate, Authorization, Logging, Caching, Validating, Timeout, then the handler (:61-:69). The query chain carries aValidatingQueryDecoratortoo (:67), so the two lists differ only by the transactional stage. Both are asserted byAssertPipeline(:79-:98), which compares every element except the last against the expected list (:93-:94) and then requires the innermost element not to end in "Decorator" (:96-:97), so a truncated chain cannot pass. ADC overrides neither expected list, so it accepts the framework default order as its contract.
- Why it's built this way - the pair was chosen for realism rather than convenience. ChangePreferencesCommand and its handler are shipped ADC Identity use cases, while GetUserPreferencesQuery is declared in
MMCA.Common.Applicationand its concrete GetUserPreferencesHandler lives in ADC on top of Common's GetUserPreferencesHandlerBase<TUser>. So the scan-then-decorate ordering is exercised across the framework and app boundary rather than against a fixture, which is exactly what the base asks a subclass to supply (DecoratorPipelineOrderTestsBase.cs:26-:27). - Where it's used - not an independent class any more:
ComposedPipeline_LeavesNoHandlerUndecorated's own base (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs) exercises the same helper, and MiddlewarePipelineOrderTests is a sibling gate over the same registration sequence at the middleware layer rather than a consumer of this type. - Caveats / not-in-source - the nesting chain is still unwrapped by reading compiler-generated private fields (
DecoratorPipelineOrderTestsBase.cs:105-:125), so a future decorator that stores its inner handler somewhere other than a field (a property-only or captured-closure design) would be invisible to that walk.VerifyDecoratorPipeline()proves every handler is wrapped by something, but does not itself replay the exact wrapper order for each one; that is still the base's job. The base flags the reflection strategy explicitly (:29-:32).
AdcArchitectureMap
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests·MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14· Level 12 · class (internal, sealed)
- What it is - the single declaration of what "the ADC architecture" is, in assembly terms: five MMCA.Common framework layers, the Identity, Conference, and Engagement modules at six layers each, and the thin Notification module at three, 26 entries in all (
MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:18-:55). Every map-driven rule in this unit scans exactly the assemblies listed here. - Depends on - ArchitectureMapBase (which implements IArchitectureMap), the Layer enum and the LayerRef record, and
System.Reflection.Assembly(global-used atMMCA.ADC.Architecture.Tests/GlobalUsings.cs:1). Through its anchor types it also depends on Result, BaseEntity<TIdentifierType>, EntityQueryService<TEntity, TEntityDTO, TIdentifierType>, ApplicationDbContext, and ApiControllerBase on the framework side, and on User, Event, UserSessionBookmark, UserNotificationExportItemDTO, their sibling DTOs, and the four IdentityModule / ConferenceModule / EngagementModule / NotificationModule entry points on the app side. - Concept introduced, the architecture map as data. NetArchTest works on
Assemblyobjects, so a fitness function needs some way to know which assembly plays which role. Rather than hard-coding assembly names inside each rule, MMCA reifies the answer in one object: a flat list of(module, layer, assembly)triples. ArchitectureMapBase derives everything else from that list: the module-name set, per-layer projections, namespace derivation, and the module-isolation target sets (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:28-:72). The consequence is that adding a module to ADC is a three-line change here, after which roughly three dozen rules start covering it.[Rubric §3 - Clean Architecture]assesses whether layer boundaries are real rather than aspirational, and this is the machine-readable statement of those boundaries.[Rubric §7 - Microservices Readiness]: the map'sModulegrouping is what lets the isolation and extraction rules ask "could this module leave the process", which for ADC is not hypothetical, since every module already runs as its own service host. - Walkthrough
RepoToken => "MMCA.ADC"(AdcArchitectureMap.cs:16) is more load-bearing than it looks. The base composes namespaces from it ({RepoToken}.{Module}.{Segment},ArchitectureMapBase.cs:63-:66,:98-:99), and the doc/config rules resolve the repository root by walking up from the test binary until they find{RepoToken}.slnx(ArchitectureMapBase.cs:79-:91), which is how DataResidencyTests, FormsConventionTests, and RawQueryableConventionTests read committed files regardless of the runner's working directory.- Framework layers (
AdcArchitectureMap.cs:21-:25) are declared with theFramework(layer, assembly)helper, which records an empty module name (ArchitectureMapBase.cs:94-:95), so they are excluded from the per-module projections. Each is pinned by an anchor type rather than a string:Resultfor Shared,BaseEntity<>for Domain,EntityQueryService<,,>for Application,ApplicationDbContextfor Infrastructure, andApiControllerBasefor API. A rename or a package move breaks the compile instead of silently producing an empty scan. - Module layers (
:28-:54) use the instanceModule(name, layer, assembly)helper. Identity, Conference, and Engagement each declare six layers; Domain, Shared, and API are pinned by anchor type (Identity.Domain.Users.User,Conference.Shared.Events.EventDTO,Engagement.API.EngagementModule, and their siblings) while Application, Infrastructure, and UI are loaded by name throughAssembly.Load(for example:29,:30,:33), because those assemblies have no convenient public anchor type (:5-:6).Assembly.Loadsucceeds here only because the csproj takes aProjectReferenceon all 21 module projects (MMCA.ADC.Architecture.Tests.csproj:46-:71), which is what puts the DLLs beside the test binary. - Notification (
:52-:54) declares three layers, not six: Application (loaded by name), Shared (pinned byNotification.Shared.UserNotifications.UserNotificationExportItemDTO), and Api (pinned byNotification.API.NotificationModule). The class comment states why (:7-:12): Notification is a deliberately thin module that owns no aggregate and no persistence, so it ships no Domain, Infrastructure, or UI assembly, and LayerDependencyTests records that shape as a per-module required-layer override rather than letting the whole repo drop the requirement. - Laziness is inherited:
DefineLayers()is materialized once through aLazy<IReadOnlyList<LayerRef>>built in the base constructor (ArchitectureMapBase.cs:13-:16), so the three dozen subclasses that each construct their own map instance still pay theAssembly.Loadcost only on first use.
- Why it's built this way - centralizing every namespace and assembly string in one file also fixes Ubuntu CI case sensitivity in one place, which the base states as an explicit goal (
ArchitectureMapBase.cs:8-:9). Compare CommonArchitectureMap, the same abstraction for a repo with no business modules. - Where it's used - instantiated as a field initializer by every map-driven subclass in this unit (34 of the 43 types here, all of them at Level 13), for example
ConcurrencyConventionTests.cs:5. It isinternal, so it never leaves this assembly. The nine non-map types are the two embedded-resource guards, AnonymousEndpointTests, ProtoContractTests, SortableColumnConventionTests, TranslationCompletenessTests, the two pipeline-order tests, and the map itself. - Caveats / not-in-source - the four
*.Contractsservice projects underSource/Services/are not registered, so no map-driven rule scans the gRPC adapters directly; ProtoContractTests covers that surface by file path instead. Nothing in this repository asserts that the map lists every module that exists, so a fifth module would have to be added here by a human.
MiddlewarePipelineOrderTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16· Level 12 · class (public, sealed)
- What it is - the HTTP-edge counterpart of DecoratorPipelineOrderTests, and, like ObservabilityConventionTests, a bodyless declaration:
public sealed class MiddlewarePipelineOrderTests : MiddlewarePipelineOrderTestsBase;(MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16). Deriving it is the whole assertion. - Depends on - MiddlewarePipelineOrderTestsBase from the
MMCA.Common.Testingpackage (using MMCA.Common.Testing;,:1), and transitively on MiddlewarePipelineBuilder and MiddlewarePipelineStepNames. - Concept introduced, an empty subclass as a conformance claim. The class comment states what the emptiness means (
:5-:14): every ADC REST and gRPC service host calls the zero-argumentUseCommonMiddlewarePipeline(), so the framework's default step order is ADC's contract and the base needs no overrides. The two hooks that exist are the escape hatch a host would use if it customized the pipeline:Configure, which defaults to null (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:37), andExpectedStepNames(:38-:58). Leaving both alone is a positive statement, not an omission.[Rubric §12 - Performance & Scalability]assesses whether cross-cutting behavior is applied uniformly rather than per host;[Rubric §11 - Security]is the sharp edge, because the ordering invariants below are authentication and rate-limiting invariants. - Walkthrough - no members. Two inherited facts run against a builder seeded from
MiddlewarePipelineBuilder.CreateDefault()(MiddlewarePipelineOrderTestsBase.cs:81-:84).EdgePipeline_OrdersSteps_InDocumentedOrder(:61) comparesbuilder.StepNamesagainst the 18-step default sequence, outermost first: exception handler, correlation id, request localization, pre-forwarded capture, forwarded headers, HTTPS redirection, response compression, routing, CORS, authentication, tenant resolution, rate limiting, soft-deleted-user filter, authorization, output cache, JWKS endpoint, OIDC discovery endpoint, controllers (:40-:57).EdgePipeline_SatisfiesLoadBearingInvariants(:70) callsBuild()and requires it not to throw, becauseBuild()re-checks the load-bearing adjacencies at startup, so a pipeline that failed here would have thrown while the host was starting (:73-:76).- Four adjacencies are named as load-bearing in both the base and the ADC comment: the pre-forwarded capture immediately before the forwarded-headers rewrite (or
jwks_uristops being reachable), authentication immediately before tenant resolution (so the claim strategy seesHttpContext.User), authentication before the rate limiter per ADR-019 (so the per-user cap engages), and forwarded headers before the HTTPS redirect (MiddlewarePipelineOrderTests.cs:10-:13,MiddlewarePipelineOrderTestsBase.cs:68).
- Why it's built this way - a reorder here fails at runtime in ways that look like configuration bugs: an unreachable
jwks_uri, a tenant that never resolves, a per-user rate cap that never engages (MiddlewarePipelineOrderTestsBase.cs:16-:18). Making it a red test in the consumer repo is what turns a framework-side reorder into a build failure rather than a silent production behavior change (ADR-079). NoWebApplicationis built: the steps are pure data until they are applied, so this runs in the fast unit tier with no database and no host (:24-:27). - Caveats / not-in-source - the test asserts the framework default, seeded from
CreateDefault(). It does not read any ADCProgram.cs, so the claim that every ADC host calls the zero-argument overload is asserted by the comment, not by this test. A host that started passing a customization would silently fall outside this gate unless someone also overrodeConfigurehere.
CommandValidatorCoverageTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18· Level 13 · class (public, sealed)
- What it is - the gate that every data-carrying command has something for the Validating decorator to run. It freezes the 26 entries that are exempt today, and fails on any new one (
MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:23-:86). - Depends on - CommandValidatorCoverageTestsBase and AdcArchitectureMap.
- Concept introduced, the allowlist grouped by reason rather than by name. The list is not alphabetical: it is split into three commented buckets, because the entries are not equally benign and a reviewer needs to see which kind an entry is (
:25-:30). That is the difference between an inventory and a mute button.[Rubric §6 - CQRS & Event-Driven]assesses whether the pipeline stages actually do work; the Validating decorator is present for every command whether or not a validator resolves, so a missing validator is a stage that runs and asserts nothing.[Rubric §11 - Security]and[Rubric §24 - Forms/Validation/UX Safety]: the base's framing is that a command with no validator carries whatever the caller sent straight into the handler, and the gap is invisible until bad input reaches the domain (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-:7). - Walkthrough - three members.
Map(:20).AllowedUnvalidatedCommands(:23-:85) overrides the base's empty default (CommandValidatorCoverageTestsBase.cs:31) with 26 entries in three groups. (1) Identifier-only commands (:25-:61), twenty-two entries whose whole payload is a server-supplied or route-supplied id plus, in a few cases, aRowVersionor an enum the compiler already constrains: seventeen remove/publish/link/delete commands across Conference and Engagement, the live-pollClose,DeleteandOpenamong them (:31-:47), ModerateQuestionCommand with its own note that every field is an id or type-constrained and the authorization decision is the handler's (:49-:51), SetLeaderboardParticipationRequest because "there is no invalid value of a bool" (:53-:54), two Identity deletes (:56-:57), and the framework's genericMMCA.Common.Application.UseCases.Crud.DeleteEntityCommandclosed over ADC aggregates, which ADC does not author (:59-:61). (2) False positives of the scan (:63-:77): ForgotPasswordCommand and ResetPasswordCommand (:70-:71), plus the email-confirmation pair (ADR-116),SendEmailConfirmationCommandandConfirmEmailCommand(:76-:77), all four carrying anICommandWithRequest<T>whoseIValidator<T>is real but lives inMMCA.Common.Application, and the rule reads only the repo's per-module Application assemblies, so it cannot see the framework half of the bridge. The comment ends with an instruction to a future maintainer: these are validated at runtime, do not "fix" them by writing a duplicate. (3) Frozen debt (:79-:85): empty, and the comment records what left it (the two question-answer updates,ChangePreferences, andSetUserAvatareach now carry a validator in their own slice).MinimumCommands => 55(:93) raises the base floor of 1 (CommandValidatorCoverageTestsBase.cs:38). The comment states the measurement it sits under: the scan found 62 handled, data-carrying commands at adoption, so the floor leaves room for normal churn while still failing loudly if a module drops out of the map (:88-:92).- Two facts are inherited:
Commands_ShouldHave_ValidationCoverage(CommandValidatorCoverageTestsBase.cs:41) and the non-vacuity checkCommandInventory_ShouldNotBe_Empty(:45).
- Why it's built this way - the base explains why both halves of the request bridge are required for a command to count as covered: the framework registers the bridge validator for every
ICommandWithRequest<TRequest>whether or not a request validator resolves, and one that resolves none adds no rules (CommandValidatorCoverageTestsBase.cs:9-:15). Counting the bridge without the request validator would have made the gate pass on exactly the commands it exists to catch. - Caveats / not-in-source - group (2) is a stated limitation of the scan rather than a property of ADC, so an ADC command that legitimately binds a framework request validator in future would land in the same bucket and would need the same hand-written note.
ClockReadTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8· Level 13 · class (public, sealed)
- What it is - the map-only ADC subclass of the framework's clock-read fitness function: the whole body is one line supplying AdcArchitectureMap (
MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:8-:11). - Depends on -
ClockReadTestsBasefromMMCA.Common.Testing.Architectureand AdcArchitectureMap. - Concept introduced - the same thin-subclass shape as BrandColorTokenTests and AiDependencyIsolationTests: the rule body lives once in the shared base, and deriving a sealed subclass over ADC's map is what turns it on for this repo (ADR-015). The base's own concern (proved once in MMCA.Common and exercised here by name only) is that direct reads of
DateTime.Now/DateTime.UtcNow/DateTimeOffset.Nowinside domain and application code are untestable and non-deterministic; production code is expected to take an injected clock abstraction instead.[Rubric §14 - Testability]is the direct target. - Walkthrough - one member,
Map(:10), implementing the base's abstract property over ADC's assemblies. - Why it's built this way - identical rationale to every other map-only subclass in this unit: the fixture proof lives once in MMCA.Common (see
MMCA.Common.Architecture.Tests/Domain/ClockReadTests.csandMMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs), so each consumer repo need only wire its own map to gain the coverage. - Where it's used - runs with the rest of the suite in the
build-and-testjob; not consumed by any other type in this unit.
CascadeSoftDeleteConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12· Level 13 · class (public, sealed)
- What it is - the guard that an aggregate root which owns auditable children deletes them in its own
Delete()override, carrying exactly one reviewed exemption (MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:17-:26). - Depends on - CascadeSoftDeleteConventionTestsBase and AdcArchitectureMap.
- Concept introduced, the consequence of soft delete not being a delete. MMCA never removes a row: an entity sets
IsDeleted = trueand the EF global query filter hides it. That means nothing cascades for free, because a soft delete is an ordinary UPDATE. Without an explicit cascade the root vanishes behind the filter while its child rows stay active, orphaned and unreachable through the root, yet still present for exports, reports, and erasure requests (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:4-:9). The framework'sDeleteChildren<TChild, TChildId>(...)helper is the one-line fix, and a hand-rolled loop over the children counts too.[Rubric §8 - Data Architecture]assesses whether the persistence model's invariants hold end to end;[Rubric §30 - Compliance/Privacy/Data Governance]is the sharper edge, since an orphaned child row is data a subject-access or erasure request has to account for (ADR-005). - Walkthrough - two members.
Map(:14), andAllowedCascadeExemptTypes(:17-:26), which overrides the base's empty default (CascadeSoftDeleteConventionTestsBase.cs:27) with a single entry,MMCA.ADC.Conference.Domain.Speakers.Speaker. The inherited fact isAggregatesWithChildCollections_MustCascadeSoftDelete_InDelete(:30). - Why it's built this way - the exemption is justified in place rather than in a wiki (
CascadeSoftDeleteConventionTests.cs:19-:24): Speaker's junction rows (SpeakerCategoryItems,SpeakerQuestionAnswers) deliberately outlive the soft-deleted root per BR-70/BR-71, because the Sessionize import reactivates them in place when the speaker returns (BR-135) and no cascade-restore counterpart exists, so cascading here would silently drop a returning speaker's categories and answers. The safety argument is the second half: junction reads follow the parent's visibility (BR-132), so the surviving children are not observable while the speaker is deleted. The base frames the whole exemption list this way, as the point of the rule rather than a concession: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (CascadeSoftDeleteConventionTestsBase.cs:11-:15). - Caveats / not-in-source - the entry is a type full name, so the exemption is matched by string; the reactivation behavior it depends on (BR-135) is asserted by the Sessionize import tests, not here.
AiDependencyIsolationTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14· Level 13 · class (public, sealed)
- What it is - the model-boundary fitness function for ADC: a language-model dependency must stay confined to Infrastructure and to the governed
MMCA.Common.AIpackage, in both directions. Map-only shape (ConcurrencyConventionTests): the whole body is one line supplying the map (MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:16). - Depends on - AiDependencyIsolationTestsBase and AdcArchitectureMap.
- Walkthrough - one member,
Map(:16), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:21). Two inherited facts:LanguageModelSdks_ShouldStayAt_TheModelBoundary(:24) fails on a module naming the vendor SDK directly (Anthropic,OpenAI,Azure.AI, orMicrosoft.Extensions.AIitself) outside Infrastructure, andGovernedAiPackage_ShouldStayBehind_Infrastructure(:28) fails on a module taking the framework's governedMMCA.Common.AIpackage as a convenience and passing aPromptContractaround as a domain type. - Why it's built this way - the base's summary states the reason both directions are checked: either leak turns "the app calls a model at one place" into "the model is spread through the app", and a dependency that is everywhere cannot be versioned, evaluated, capped or swapped (
AiDependencyIsolationTestsBase.cs:7-:12).[Rubric §16 - AI-Native Development]is the direct target.
ConcurrencyConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the guard that no
*UpdateRequestcarries a concurrency token in its body. It is also the plainest example of the Level 13 shape in this unit: a sealed class whose entire body is one line supplying the map (MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:5). - Depends on - ConcurrencyConventionTestsBase and AdcArchitectureMap.
- Concept introduced, the map-only subclass. Eighteen of the 34 map-driven types in this unit are exactly this:
protected override IArchitectureMap Map { get; } = new AdcArchitectureMap();and nothing else. Note the property is an auto-property with an initializer, not an expression body, so each class constructs its map once per test-class instance rather than per fact. Everything else (the rule bodies, the[Fact]attributes, the failure messages) is inherited, which is precisely the point: MMCA.Common, MMCA.Store, and MMCA.ADC run byte-identical rule logic and differ only in what they point it at. The sections below for the other map-only subclasses do not repeat this explanation; they name the base and list what it asserts.[Rubric §15 - Best Practices & Code Quality]assesses duplication and change cost: a new rule ships to all three repos by adding a base and one derived line per repo. - Walkthrough - one member,
Map(:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:11). The inherited fact isUpdateRequests_ShouldNotImplement_IConcurrencyAware(:14), which delegates toArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map).[Rubric §8 - Data Architecture]and[Rubric §9 - API & Contract Design]: the rule enforces a single source for the token. The version a conditional update is checked against is read from theIf-Matchrequest header, so a token in the request body would give the same check a second, competing source (ConcurrencyConventionTestsBase.cs:4-:7). That is the inverse of what the same-named rule asserted before the header became the only path, and it is why IConcurrencyAware must now be absent from request bodies rather than present. The base notes that modules with no mutable aggregate are legitimately vacuous here (:7). - Where it's used - runs with the whole suite in the
build-and-testjob (MMCA.ADC/.github/workflows/deploy.yml:189,:284). The same is true of every remaining type in this unit and is not repeated below.
ConstructorDependencyCountTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20· Level 13 · class (public, sealed)
- What it is - a single-responsibility ceiling applied to three separate populations: no
*Servicein a mapped module Application assembly may exceed eight constructor dependencies, no API controller may exceed ten, and no CQRS handler may exceed seven (MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:22-:31,:59,:68). - Depends on - ConstructorDependencyCountTestsBase and AdcArchitectureMap.
- Concept introduced, the ratchet set to the real high-water mark, and its history kept in the file. A ceiling is only a ceiling if it sits at the current maximum, so the class comment keeps a ledger of every move, on the stated principle that "a ceiling only means something if every move of it was deliberate". It was 8 while the comment claimed AuthenticationService had 8 dependencies; it had 7, so the gate carried a phantom slot of headroom and was tightened to the real mark on 2026-07-28. It moved to 9 when refresh tokens became multi-device sessions: the framework's
AuthenticationServiceBaseconstructor took on the session store and its options, and ADC's subclass passed both through on top of theIExternalLoginEmailVerifierits OAuth auto-link gate needs. It moved again, 9 to 10, on 2026-09-23: theAuthenticationServiceSettingsholder that had bundledIOptions<RefreshSessionSettings>andIOptions<EmailConfirmationSettings>into one slot was deleted, and both options now reach the framework base directly (ConstructorDependencyCountTests.cs:24-:27). It then moved down, 10 to 8, on 2026-10-06: Common 1.218.1 folded the session store intoIAuthSessionIssuer, soAuthenticationServicetakes 8, and the base'sApplicationServices_ConstructorDependencyCeilingIsTightnow fails whenever the ceiling is left above the observed mark (:28-:31). The comment then makes the judgement explicit each time: the additions are framework-imposed collaborators of the same facade rather than a second responsibility, so the honest response is a raised ceiling and not an artificial bundle.[Rubric section 1 - SOLID]assesses single responsibility among other things; constructor arity is the cheapest mechanical proxy for a class that has accumulated too many jobs, and the comment is careful to distinguish a cohesive facade from a bundle. - Walkthrough - four members.
Map(:22), andMaxConstructorDependencies => 8(:31), which implements the base's abstract service ceiling (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:43) and backsApplicationServices_DoNotExceedConstructorDependencyCeiling(:52), which scans every non-abstract class whose name ends in "Service" in the map's module Application assemblies (:54-:58), asserts the set is non-empty so the guard cannot pass vacuously (:60-:61), and reports each offender with its own arity (:63-:80). The subclass comment names AuthenticationService, now at the mark of 8, and CreateSessionHandler next at 7 (ConstructorDependencyCountTests.cs:6-:8). The other two members arrived with the framework's 2026-09 wave, which added a controller ceiling and a handler ceiling to the base alongside the existing service one.MaxControllerConstructorDependencies => 10(:59) implements the base'sControllers_DoNotExceedConstructorDependencyCeiling(ConstructorDependencyCountTestsBase.cs:119), which scans every concreteControllerBase-derived class across the map's API assemblies. Its own doc comment keeps the same kind of ledger: the fact first reported 14 (SpeakersController), 12 (EventsController), 11 (SessionsController) and 10 (LivePollsController), each serving several sub-resources from one class; each was split by sub-resource into sibling controllers on the same route prefix, action templates and authorization attributes, so no URL, verb or authorization posture moved (SpeakerLinksController/SpeakerSessionsController14 to 9,EventLifecycleController12 to 9,SessionCalendarController11 to 10,LivePollVotingController10 to 7).SessionsControllerstops at 10 rather than 9 deliberately: its remaining dependency isIEntityQueryService<Event, ...>, held only for the BR-86 advisoryX-Warningheader on create, which the update path gets fromUpdateSessionResult.HasDateRangeWarningbut the create path cannot, because the framework base fixes its create handler atICommandHandler<SessionCreateRequest, Result<SessionDTO>>and the Application layer has no way to set a response header; the honest mark is therefore 10, to be lowered the moment a framework change makes 9 true (ConstructorDependencyCountTests.cs:36-:58).MaxHandlerConstructorDependencies => 7(:68) implementsHandlers_DoNotExceedConstructorDependencyCeiling(ConstructorDependencyCountTestsBase.cs:143), scanning every concreteICommandHandler<,>/IQueryHandler<,>implementation in the map's module Application assemblies, set at the mark the fact first reported: ResetPasswordHandler and SubmitQuestionHandler jointly at 7, with CreateSessionHandler,RefreshFromSessionizeHandler,UploadSessionAssetHandler,CreateBookmarkHandlerandRecordRoomCheckInHandlernext at 6 (:62-:67). - Why it's built this way - each ceiling is meant to be raised consciously rather than drifted past, which is what the comments ask for and what their own history demonstrates. The controller split is the sharpest example: four controllers that had accreted several sub-resources were split along existing route and authorization boundaries rather than pinned at their old, inflated marks.
- Caveats / not-in-source - the sibling rule HandlerConventionTests raises the base's separate arity check to 10 for a different population (
HandlerConventionTests.cs:18), and its<remarks>states the coupling directly: the two guards must move together, or the stricter one turns the other into a comment (:11-:12). Note the service scan is name-based (*Service), so a handler or a builder with the same arity is judged only by its own ceiling, not this one.
ContractImplementationTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:20· Level 13 · class (public, sealed)
- What it is - the encapsulation half of the
[ServiceContract]boundary: the concrete class serving a published contract interface must not be public. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:22). - Depends on - ContractImplementationTestsBase and AdcArchitectureMap.
- Concept introduced, guarding a boundary from both sides. This rule is the twin of ServiceContractPurityTests, and the base says so in as many words: purity keeps the producer's internals out of the contract, this keeps the contract's implementation out of the consumer's reach (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:11-:13). A public implementation lets a consumer reference, construct, or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (:5-:8).[Rubric §7 - Microservices Readiness]assesses whether extraction is reversible in practice;[Rubric §1 - SOLID]: this is dependency inversion enforced at the assembly boundary rather than only at the call site (ADR-007). - Walkthrough - one member,
Map(:21), implementing the base's abstract property (ContractImplementationTestsBase.cs:22). One inherited fact,ServiceContractImplementations_ShouldNotBe_Public(:33), and theAllowedPublicImplementationshook (:30) is left at its empty default. The subclass documentation is where the real teaching is (ContractImplementationTests.cs:3-:18): ADC's six contracts (IAttendeeQueryService,IBookmarkCountService,IEventLiveValidationService,ISessionBookmarkValidationService,IUserEngagementExportService,IUserNotificationExportService) each have three implementations, the in-process one in*.Application, theDisabled*stub in*.Shared, and the gRPC adapter inServices/*.Contracts, and all of them areinternal sealed. The<remarks>explains why the allowlist stays empty: nothing in ADC ships a contract implementation a consumer is meant to construct, and the assemblies that register or exercise these classes reach them throughInternalsVisibleToon the producing projects rather than through a public type (:14-:18). - Caveats / not-in-source - the rule is scoped to the assemblies AdcArchitectureMap registers, and the
Services/*.Contractsprojects are not among them, so the gRPC adapters'internalvisibility is asserted by the class comment rather than by this test.
ControllerConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the API-layer convention guard, with a two-entry exemption list for the controllers that legitimately do not route through the framework's base controller (
MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:11-:15). - Depends on - ControllerConventionTestsBase, AdcArchitectureMap, and, by name only (they are strings, not type references), OAuthController and ServiceInfoController.
- Concept introduced, the documented exemption. A rule with no escape hatch gets deleted the first time reality disagrees with it; a rule with an undocumented escape hatch rots. The middle path here is an allowlist of fully qualified names, each justified in the comment above it (
:7-:10): the OAuth controller drives a redirect, challenge, and cookie flow with an out-of-band token exchange, and the service-info controller is an anonymous version-discovery diagnostic, so neither returns a domainResultand neither has anything to gain from theResult-to-HTTP mapping in ApiControllerBase.[Rubric §9 - API & Contract Design]assesses consistency of the HTTP surface; the rule keeps the default consistent while naming the two deliberate outliers. - Walkthrough - two members.
Map(:5) andControllersExemptFromApiControllerBase(:11-:15), which overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12). Four facts are inherited: controllers do not depend on Infrastructure (:15), do not depend on EF Core (:18), are sealed (:21), and inheritApiControllerBaseexcept for the exempt names (:24). - Caveats / not-in-source - the exemptions are matched as strings, so renaming or moving either controller silently drops it from the list and turns the rule red rather than passing wrongly, which is the safe direction.
DataResidencyTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12· Level 13 · class (public, sealed)
- What it is - a compliance drift guard: the data-residency statement published in ADC's
PRIVACY.mdmust match the Azure region where personal data is actually provisioned, parsed out of the deployment workflow (MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:3-:11). - Depends on - DataResidencyTestsBase, AdcArchitectureMap,
System.IO.File/Path, and AwesomeAssertions (used directly inside the override,:26). - Concept introduced, a test as the join between a document and an infrastructure fact. Most of the rules in this unit compare code to code. This one compares prose to infrastructure: it reads the deployed region out of the source of truth (
SQL_LOCATION="${SQL_LOCATION_OVERRIDE:-westus2}",MMCA.ADC/.github/workflows/deploy.yml:1158) and then requiresPRIVACY.mdto say the same thing, comparing whitespace-insensitively and case-insensitively so "West US 2" matches thewestus2region token.[Rubric §30 - Compliance/Privacy/Data Governance]assesses whether privacy claims are true and stay true; a policy that names a region the data never lived in is a compliance defect that no code review would catch, and this is the mechanism that closes it. - Walkthrough - three members.
Map(:14) exists only so the base can resolve the repo root throughArchitectureMapBase.FindRepoRoot($"{Map.RepoToken}.slnx"); no assembly scanning happens in this rule.ForbiddenResidencyClaims => ["central United States"](:16) overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:24) and blocks a specific stale statement from returning, one that once contradicted the deployed region (DataResidencyTests.cs:9-:10).ExtractDeployedRegion(string repoRoot)(:20-:31) implements the base's abstract hook (DataResidencyTestsBase.cs:87). It reads.github/workflows/deploy.yml(:22), locates the literal markerSQL_LOCATION_OVERRIDE:-with an ordinalIndexOf(:24-:25), asserts the marker exists with abecauseexplaining what the workflow must declare (:26-:27), then takes the alphanumeric run that follows as the region (:29-:30). Assert-then-parse rather than return-empty is what the base asks implementations to do.- The inherited fact
PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion(DataResidencyTestsBase.cs:27) then asserts the normalized policy contains the normalized region and contains none of the forbidden claims.
- Why it's built this way - the account data and session bookmarks live in the Azure SQL database, and the QiMata Sponsorship subscription forces that SQL server into a different region from the Container Apps (
DataResidencyTests.cs:5-:9), so "where the app runs" is genuinely not "where the personal data sits". Parsing the SQL region default rather than the app region encodes that distinction. - Caveats / not-in-source - the parse is positional: it takes the first occurrence of the marker in
deploy.yml. The workflow contains both a job-levelSQL_LOCATION_OVERRIDEenv binding (deploy.yml:1111) and the shell default (:1158), so the rule depends on the marker stringSQL_LOCATION_OVERRIDE:-appearing only in the latter form.
DomainEventHandlerSaveTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11· Level 13 · class (public, sealed)
- What it is - the rule that a domain-event handler mutates state and lets the owning unit of work flush it, rather than persisting on its own. It carries two allowlisted entries (
MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:16-:28). - Depends on - DomainEventHandlerSaveTestsBase and AdcArchitectureMap.
- Concept introduced, the transitive call-graph rule. Every other assembly-scanning rule in this unit judges a type by its own declarations. This one walks the call graph out of every handler method, so the common real shape (handler to a domain service to
SaveChangesAsync) is caught rather than only a save typed directly into the handler (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:10-:14). The walk depth is bounded byMaxCallDepth, defaulted to 6, which the base describes as covering the realistic handler-to-service-to-helper-to-repository chain while keeping the scan fast (:35-:40); ADC does not override it.[Rubric §6 - CQRS & Event-Driven]assesses the discipline around dispatch;[Rubric §8 - Data Architecture]: dispatch runs afterSaveChangesAsync, and after commit inside anITransactionalcommand, so a handler that saves opens a second write in the middle of the first one, re-entering the change tracker, potentially raising a fresh event cascade, and persisting work the outer transaction may still roll back (:4-:9). - Walkthrough - two members.
Map(:13), andAllowedSavingTypes(:16-:28), which overrides the base's default (DomainEventHandlerSaveTestsBase.cs:33) with two entries. The first,"MMCA.Common"(:20), is the base's own default carried forward: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect the rule hunts (:18-:19). The second is PointsAwarder (:27), labelled an accepted deliberate trade-off: the gamification points ledger is written by the awarder that the point-scoring domain-event handlers call, so awarding points is a second write that follows the first, and the comment states plainly that the flow is intentional and is not to be refactored onto the outbox, because a point award that lost its triggering write would be worse than one that trails it (:22-:26). The inherited fact isDomainEventHandlers_ShouldNotReach_SaveChanges(DomainEventHandlerSaveTestsBase.cs:43). - Why it's built this way - an allowlist entry both silences the type and stops the walk from descending into it (
DomainEventHandlerSaveTestsBase.cs:16-:19), which is why the class comment insists the two accepted ADC cascades are named explicitly rather than silenced module-wide (DomainEventHandlerSaveTests.cs:8-:9): a namespace-wide entry would have hidden whatever lies beyond it too. - Caveats / not-in-source - the depth bound is 6, so a save reached through a deeper chain is outside the walk. The base names that as a tuning knob rather than a guarantee.
DomainPurityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3· Level 13 · class (public, sealed)
- What it is - the Clean Architecture purity guard, plus one repo-specific addition: RabbitMQ is added to the forbidden-dependency list for Domain and Shared (
MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:9). - Depends on - DomainPurityTestsBase and AdcArchitectureMap.
- Concept introduced - the map-only shape from ConcurrencyConventionTests, extended by a one-line hook. The extra entry is not decoration: ADC runs on a broker (RabbitMQ locally, Azure Service Bus in production,
:7-:8), and a broker client reference inside Domain would tie the model to a transport.[Rubric §3 - Clean Architecture]assesses inward-only dependencies;[Rubric §7 - Microservices Readiness]: keeping the transport out of the core is what makes IMessageBus substitutable between in-process and broker delivery. - Walkthrough - two members:
Map(:5) andExtraForbiddenDomainDependencies => ["RabbitMQ"](:9), overriding the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12). Four facts are inherited: Domain is framework-free (:15), Shared is framework-free (:18), Application does not depend on EF Core (:21), and Application does not depend on ASP.NET Core (:24). The extra token is passed to the first two only.
DomainThrowTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9· Level 13 · class (public, sealed)
- What it is - the Result-pattern purity guard: no
throwin ADC's Domain assemblies except an argument guard. The allowlist is present and empty (MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:14-:17). - Depends on - DomainThrowTestsBase and AdcArchitectureMap.
- Concept introduced, the cost of a thrown business failure. The framework's whole error model is that a business outcome comes back as a
Result(ADR-013), and the base names the three concrete consequences of throwing one instead: it skipsResult.Combineinvariant composition, it arrives at the API as a 500 rather than an outcome the caller can act on, and it pays an exception unwind on a path that is not exceptional (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:4-:9). Argument guards stay allowed because they report a caller bug rather than a business outcome, and the modern guard style always passes anyway: a barethrow;rethrow is ignored, and theArgumentNullException.ThrowIfNullfamily emits nothrowin the caller at all (:11-:12).[Rubric §15 - Best Practices & Code Quality]assesses whether a stated convention actually holds;[Rubric §9 - API & Contract Design]: which failures a caller can handle is part of the contract. - Walkthrough - two members.
Map(:11), andAllowedThrowingTypes(:14-:17), which overrides the base's empty default (DomainThrowTestsBase.cs:31) with a collection expression containing only a comment,TRIAGE PLACEHOLDER: filled from the first run(DomainThrowTests.cs:16). Since the list is empty and the rule is in the CI suite, that comment is now a historical note rather than an open item: ADC's Domain assemblies throw nothing beyond the argument guards, or the gate would be red. The inherited fact isDomain_ShouldNotThrow_ExceptArgumentGuards(DomainThrowTestsBase.cs:34). - Why it's built this way - the base spells out the adoption path the placeholder anticipated (
:14-:19): subclass, run once, then either convert each reported site to aResult.Failureor move it into the list with a comment. ADC took the first branch everywhere, which is why the list stayed empty. - Caveats / not-in-source - the exception filter is by type (
ArgumentException,ArgumentNullException,ArgumentOutOfRangeException), so a custom guard exception outside that family would be reported.
EntityConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the DDD entity-shape guard for ADC's three module domains: aggregate roots exist, each has a
Result-returning static factory and no public constructor, domain entities are sealed and live in the Domain layer, entity properties carry no public setters, and DTOs or requests do not live in Domain or Infrastructure. - Depends on - EntityConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Eight inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15,:18,:21,:24,:27,:30,:33,:36) cover aggregate-root exposure, theResult-returningCreatefactory, constructor visibility, factory return types, sealing, layer placement, non-public property setters, and the DTO/request exclusion.[Rubric §4 - DDD]assesses whether the tactical patterns are actually applied; this is the rule that keeps the factory-plus-Resultconstruction contract from being optional in a new module, and the setter check is what keeps an entity's state from being reachable without going through a behavior method.
ErrorCatalogTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20· Level 13 · class (public, sealed)
- What it is - the guard over ADC's error-code vocabulary: one literal code means one thing, and a code carries the prefix of the vocabulary that owns it. It is the second-largest subclass in the unit, at 108 lines, and almost all of it is justification (
MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:28-:59,:65-:81). - Depends on - ErrorCatalogTestsBase and AdcArchitectureMap.
- Concept introduced, treating error codes as a public contract. The class comment states the constraint that shapes every decision in this file (
:12-:18): error codes reach clients in problem-details payloads and get quoted in support tickets, so nothing already shipped is renamed to satisfy the rule. Today's catalog is locked exactly as it stands and only new drift is gated.[Rubric §9 - API & Contract Design]assesses whether the error surface is governed like the rest of the API;[Rubric §15 - Best Practices & Code Quality]: a vocabulary in which one code means two things is a vocabulary a client cannot switch on. - Walkthrough - three members plus three inherited facts.
Map(:22).AllowedCodePrefixes(:28-:59) overrides the base's default of the map's module names (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:29) because ADC namespaces its codes by aggregate (Session.Room.DoubleBooked) rather than by module. It spreadsMap.ModuleNames(:30) and then adds 22 literal aggregate prefixes in three commented groups: ten Conference aggregates and children (:33-:42,SessionAssetnewly added), nine Engagement ones (:45-:53), and three Identity ones (:56-:58). The comment frames the list as an inventory: a code under a new prefix fails until the prefix is reviewed and added, which is the drift the gate exists to catch (:8-:10).AllowedSharedCodes(:65-:81) spreads the base's three generic statics (ErrorCatalogTestsBase.cs:35-:36) and freezes seven ADC codes that two types both construct today (:74-:80). Each carries an inline note naming the pair, for exampleCheckIns.EventNotPublishedreached from CheckInProcessor,RecordRoomCheckInandRecordSponsorVisit, andSessionQuestion.InvalidTransitionreached fromModerateQuestionHandlerand the SessionQuestion aggregate. The reasoning is stated once above them (:69-:73): each pair is the same failure reported at two entry points, so the sharing is the correct behavior, a client switching on the code wants one answer rather than three near-identical ones, and renaming them would break that client. The rule is therefore told the sharing is deliberate rather than told to look away.MinimumErrorCodes => 57(:107) raises the base floor of 1 (ErrorCatalogTestsBase.cs:43), and its documentation is the most instructive passage in the file (ErrorCatalogTests.cs:83-:106). The catalog held 73 distinct non-generic codes at adoption; the floor has since been lowered twice, and both times for the same non-alarming cause: a code stopped being a literal the IL scan can see, because it moved from anErrorfactory call in an ADC module into an argument of a framework member that builds theErrorinside MMCA.Common. The comment records each move (70 to 66 when fiveX.NotDeletedcodes moved intoRestoreChild(notDeletedErrorCode: ...)arguments; 66 to 57 when eight more moved intoICurrentUserService.RequireUserId(code, ...)andCommonInvariantsmembers), notes that the codes themselves ship unchanged, and states that the scanned count after the change is 60, measured from a run rather than modelled.- The three inherited facts are
ErrorCodes_ShouldBe_Unique(ErrorCatalogTestsBase.cs:46),ErrorCodes_ShouldCarry_TheOwningModulePrefix(:50), and the non-vacuity checkErrorCodeCatalog_ShouldNotBe_Empty(:54). Prefix matching isprefix + ".", ordinal (:62-:67).
- Why it's built this way - codes are read out of IL at the
Errorfactory call sites, so a code that is not a literal cannot be judged statically; the base lists such codes as UNVERIFIABLE in the failure message rather than passing or failing them (ErrorCatalogTestsBase.cs:9-:12). That design is exactly what makes the floor drift downward as code construction moves into framework arguments, and why the subclass documents each drop instead of quietly editing the number. - Caveats / not-in-source - a scanner follow-up that would also read framework-member code arguments is described in the comment as tracked in the Wave 6 plan (
ErrorCatalogTests.cs:90-:90); nothing in this repository implements it today. Also note the floor of 57 now sits below a measured 60, so it is a margin rather than an exact count.
EventConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the integration-event shape guard: every integration event declares a schema version, inherits the framework's base integration event, and lives in an
*.IntegrationEventsnamespace under Shared; and every event upcaster is unique and moves the version forward. - Depends on - EventConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:15,:17,:20,:23,:26). The first three enforceSchemaVersion, base-type inheritance, and namespace placement (ADR-010). The last two guard the upcaster machinery that ADR-010 depends on:EventUpcasters_ShouldHave_UniqueSourceTypes(:23), so two upcasters cannot claim the same source shape, andEventUpcasters_ShouldIncrease_SchemaVersion(:26), so an upcaster cannot map an event onto the same or an earlier version.[Rubric §6 - CQRS & Event-Driven]assesses the discipline around asynchronous contracts; this rule handles the shape, while IntegrationEventContractTests freezes the content.
FeatureFlagLifecycleTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:9· Level 13 · class (public, sealed)
- What it is - the feature-flag lifecycle guard (ADR-031): every flag constant declares whether it is a permanent capability switch or a temporary rollout toggle, and a temporary one fails the build once its
RemoveBydate has passed. Map-only shape (ConcurrencyConventionTests): the body is one line (MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:11). - Depends on - FeatureFlagLifecycleTestsBase and AdcArchitectureMap.
- Concept introduced, the dead-toggle detector. A flag that gates a finished rollout stops being a choice and becomes a branch nobody takes, and nothing else in a codebase notices; the base's summary names this the open trade-off the two rules close (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FeatureFlagLifecycleTestsBase.cs:4-:8).[Rubric §31 - Observability]and[Rubric §15 - Best Practices & Code Quality]: an undeclared flag or one still gating code long after its rollout finished is exactly the kind of silent drift a fitness function, not a code review, is positioned to catch on every build. - Walkthrough - one member,
Map(:11). Two inherited facts:FeatureFlags_ShouldDeclare_Lifetime(FeatureFlagLifecycleTestsBase.cs:21), delegating toArchitectureRules.FeatureFlagsDeclareLifetime(Map), andTemporaryFeatureFlags_ShouldNotBe_PastRemoveBy(:24-:25), delegating toArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy(Map, Today).Todayis the base'svirtual DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow)(:18), which ADC does not override, so the expiry check judges against the build's own clock rather than a frozen date. - Why it's built this way - the
[FeatureFlag(FeatureFlagLifetime.Permanent|Temporary, RemoveBy, Owner)]attribute and theFeatureFlagRegistryruntime inventory it feeds are the mechanism (MMCA.Common/CLAUDE.md, ADR-031's 2026-09-11 revision); this class is ADC's adoption of the pair of fitness rules built to enforce that attribute against ADC's own flag constants, mirroring the identical subclass shape MMCA.Common carries for its own flags. - Caveats / not-in-source - adoption is per repo (ADR-031): the rule only ever sees flags declared in assemblies the map scans, so a flag constant outside those assemblies is invisible to it.
FormsConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13· Level 13 · class (public, sealed)
- What it is - the UX-safety guard over ADC's admin forms. It is now a Map-plus-floor subclass of the shared rule: the whole body is two overrides, and the requiredness and Profile facts that ADC once hand-wrote here live in the base (
MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13-:18). The<summary>still names what the nine Conference create forms must keep (:3-:12). - Depends on - FormsConventionTestsBase and AdcArchitectureMap.
- Concept introduced, a default that became the convention. ADC used to replace the base's marker list and carry two facts of its own, because the shared rule could not yet reach the model-bridge markers, the shared
*FormFields.razorcomponent, or the Profile page. The base now defaults to exactly that set, so the subclass shrank to configuration: the summary says the base's default markers are the model-bridge set ADC uses and that the Identity Profile page is checked at the base's conventional path (FormsConventionTests.cs:10-:11). Rubric tags:[Rubric §24 - Forms/Validation/UX Safety](users are protected from losing work and from unclear validation) and[Rubric §18 - UI Architecture](the field block is a shared component the scan follows). - Walkthrough
Map(:15) returns a newAdcArchitectureMap, andMinimumCreateForms => 9(:17) lifts the base default of 1 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:53) to ADC's nine Conference create forms (Activity, ConferenceCategory, Event, Partner, Question, Room, Session, Speaker, Sponsor;FormsConventionTests.cs:5-:6).- Three inherited facts run:
AdminCreateForms_KeepUnsavedChangesGuardAndValidation(FormsConventionTestsBase.cs:148) checks the ten defaultRequiredMarkers(:63-:75):UnsavedChangesGuard,IsDirtyAccessor,_isDirty,<MudForm,Model="_model",Validation="@_validate",<ErrorSummary,Result="_saveResult",Messages="_form?.Errors"andValidation.CorrectFollowing.AdminCreateForms_ReadRequirednessOffTheirModel(:156) looks forModelValidation.IsRequiredacross the create page plus its sibling*FormFields.razor.ProfileForm_KeepsErrorSummaryAndPasswordValidation(:188) checks the Profile page. - The Profile page path is the base's virtual
ProfileFormPath(:82-:92), built asSource/Modules/Identity/{RepoToken}.Identity.UI/Pages/Users/Profile/Profile.razor; ADC does not override it.
- Why it's built this way - the Profile form is a single-section password and delete form with no navigate-away step, so it carries no unsaved-changes guard and does not match the
*Create.razorglob; the base checks the markers that do apply instead (FormsConventionTestsBase.cs:86-:80). Hoisting the facts into the base means Store and any later consumer get the same coverage by subclassing. - Caveats / not-in-source -
ProfileFormPathis conventional, so moving ADC'sProfile.razorfails the test until the property is overridden (the safe direction); the ADCAGENTS.mdnotes the same.
ForwardedJwtAudienceTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Api·MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10· Level 13 · class (public, sealed)
- What it is - the fail-closed audience gate (ADR-004) for ADC's extracted service hosts: every host that calls
AddForwardedJwtBearermust pass its audience asJwtAudience.RequireConfigured(configuration[JwtAudience.ConfigKey])with no??fallback, so a host deployed withoutJwt:Audiencefails at startup rather than validating against a hard-coded default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ForwardedJwtAudienceTestsBase.cs:3-:10). The subclass is two members (MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10-:15). - Depends on - ForwardedJwtAudienceTestsBase, AdcArchitectureMap and
ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25). - Concept introduced, a security default enforced by scanning source. The rule is a source scan, not a reflection check: it walks the
*.csfiles underSourceRoots(default: the repo'sSourcefolder,ForwardedJwtAudienceTestsBase.cs:24-:25) and fails anyAddForwardedJwtBearercall whose audience is not wrapped inRequireConfigured. - Walkthrough -
Map(:12) andMinimumForwardedJwtHosts => 3(:14), the non-vacuity floor: the scan must find at least three calls, matching the base's description of "the repo's number of extracted service hosts that are not the token issuer" (ForwardedJwtAudienceTestsBase.cs:15-:19). ADC has four service modules and Identity issues the tokens, so three forwarding hosts is the expected count. One inherited fact,ForwardedJwtBearerHosts_ResolveTheirAudienceFailClosed(:27-:29). - Why it's built this way - authored once in Common and re-run as a thin subclass per repo, the same shape as the other Map-plus-number subclasses (FormsConventionTests); the floor keeps the gate from passing vacuously if a refactor moves the call sites.
- Caveats / not-in-source - the count of three is a floor, not an equality: a fourth forwarding host does not fail the test, and a host that stops forwarding only fails once the count drops below three.
FrameworkVersionConsistencyTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9· Level 13 · class (public, sealed)
- What it is - the lockstep-versioning gate: every
MMCA.Common.*package pinned in ADC'sDirectory.Packages.propsmust carry one and the same version, so a partial sweep fails CI instead of producing a subtly mismatched framework surface at runtime (MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:3-:8). - Depends on - FrameworkVersionConsistencyTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests); the map is used for its repo token, to find the props file from the repo root.
- Concept introduced - a policy made executable. ADR-016 says consumers bump every
MMCA.Common.*entry together, with no phased rollout. A policy that lives only in a document is enforced by memory; this rule enforces it by build.[Rubric §32 - Dependency & Supply-Chain]assesses how dependency versions are governed;[Rubric §15 - Best Practices & Code Quality]: a half-swept pin set is the kind of defect that surfaces as an unrelated runtime error weeks later. - Walkthrough - one member,
Map(:11). The inherited fact isAllMmcaCommonPackages_ArePinnedToOneVersion(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:25), and the base's non-vacuity floorMinimumCommonPackageCountstays at its default of 13 (:22). ADC currently pins 16MMCA.Common.*packages inMMCA.ADC/Directory.Packages.props, so the floor is met with room to spare.
HandlerConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:16· Level 13 · class (public, sealed)
- What it is - the CQRS handler placement and composition guard: handlers and validators live in the Application layer, handlers do not inject other handlers, application services do not inject handlers, domain event handlers are sealed and live in Application, and application services respect a constructor-arity limit that ADC raises to ten (
MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:18). - Depends on - HandlerConventionTestsBase and AdcArchitectureMap.
- Walkthrough - two members.
Map(:16) andMaxServiceConstructorParameters => 10(:18), raising the base default of 8 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12). Six facts are inherited (:15,:18,:21,:24,:27,:30).[Rubric §6 - CQRS & Event-Driven]assesses whether the command/query split is structural rather than nominal; a handler that injects another handler is the classic way that split quietly becomes a call graph.[Rubric §1 - SOLID]covers the arity half. - Why it's built this way - the
<remarks>(:6-:13) records both the reason for the raise and the coupling it creates. The sharedAuthenticationServiceBaseconstructor took on the refresh-session store and its options, and ADC's subclass passed both through alongside theIExternalLoginEmailVerifierits OAuth auto-link gate needs; the rationale lives in ConstructorDependencyCountTests, which sets the same ceiling. Both moved 9 to 10 on 2026-09-23 forAuthenticationService, when the options holder that bundled its two settings objects into one slot was deleted (:11-:13). The last sentence of the<remarks>is the operative one: the two guards must move together, or the stricter one turns the other into a comment. - Caveats / not-in-source - ADC therefore has two overlapping arity limits with the same value (10) but different scans (this one over application services generally, the sibling over
*Servicetypes in module Application assemblies). Whether they cover an identical type set is decided insideArchitectureRulesand is not determinable from these subclasses.
IntegrationEventPayloadPurityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:14· Level 13 · class (public, sealed)
- What it is - the guard on ADC's cross-service async contract (ADR-010): every integration event ships from a
*.Sharedassembly, and no property on its wire shape, including a nested payload record, reaches a type declared in a*.Domainassembly. - Depends on - IntegrationEventPayloadPurityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(IntegrationEventPayloadPurityTests.cs:16). Two inherited facts,IntegrationEvents_ShouldShipFrom_SharedAssembliesandIntegrationEventPayloads_ShouldNotExpose_DomainTypes(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:38,:41).[Rubric §9 - API & Contract Design]and[Rubric §7 - Microservices Readiness]: a domain event is an internal fact free to change with the model that raises it, an integration event is a public contract another service deserializes by shape and cannot be asked to redeploy because a producer renamed a field, and the two rules keep that distinction structural rather than cultural (IntegrationEventPayloadPurityTestsBase.cs:6-:10).
LayerDependencyTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3· Level 13 · class (public, sealed)
- What it is - the Clean Architecture layer-flow guard, and the highest-fact-count rule in the unit: fifteen inherited facts covering which layer may reference which, plus one override that records Notification's deliberately thin shape (
MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:15-:19). - Depends on - LayerDependencyTestsBase and AdcArchitectureMap.
- Concept introduced, the per-module exception recorded where it is true. Two of the fifteen facts are map-completeness checks:
LayerMap_DeclaresEveryExpectedLayerandLayerMap_ModulesDeclareEveryExpectedLayer(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:53,:56) require the five core layers Shared, Domain, Application, Infrastructure, and Api to be declared, both overall and by every mapped module (:16-:17,:27). ADC has one module that legitimately cannot satisfy the module half, so it uses the narrow hook rather than the broad one. The base is explicit about why that matters (:22-:25): trimmingRequiredModuleLayerswould apply to every module, so one thin module would stop the rule from catching a forgotten assembly anywhere in the repo.[Rubric §3 - Clean Architecture]is the whole point of the rule;[Rubric §34 - Architecture Governance & Documentation]: the exception is written down, in code, at the one place it is true. - Walkthrough - two members.
Map(:5), andModuleRequiredLayerOverrides(:15-:19), which overrides the base's empty dictionary (LayerDependencyTestsBase.cs:49-:50) with one entry mapping"Notification"to[Layer.Shared, Layer.Application, Layer.Api]. Its<summary>states the justification (LayerDependencyTests.cs:7-:14): Notification owns no aggregate and no persistence, so there is no Domain and no Infrastructure assembly to declare, and it has no pages of its own so there is no UI assembly either; its API project composes MMCA.Common's notification infrastructure (the SignalR hub and sender) directly rather than wrapping it in an ADC Infrastructure layer. The remaining thirteen facts (LayerDependencyTestsBase.cs:60through:96) assert the directed rules: Domain depends on neither Application, Infrastructure, nor Api; Application on neither Infrastructure nor Api; Infrastructure not on Api; Shared on nothing above it; and Ui on none of Domain, Application, or Infrastructure. Note the two-gate model described inMMCA.Common/CLAUDE.md: the same boundaries are enforced at compile time in the framework byMMCA.Common.LayerEnforcement.targets, and here at test time against compiled assemblies.
LocalizedTextConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:15· Level 13 · class (public, sealed)
- What it is - the companion to TranslationCompletenessTests. Where that one asks "is every key translated", this one asks "does every user-visible string go through a key at all": no hard-coded literals in
.razoror.razor.csunderSource/(MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:3-:14). - Depends on - LocalizedTextConventionTestsBase and AdcArchitectureMap.
- Concept introduced - the per-line escape marker. Some literals genuinely should not be translated (the conference brand name, content data), so the rule exempts them with an
i18n: allowcomment on the offending line rather than an allowlist file (:10-:11). Keeping the exemption physically next to the literal is what makes it reviewable.[Rubric §27 - i18n], executing ADR-027. - Walkthrough - the class is a thin subclass with no facts of its own: it declares only
Map(:17) andMinimumScannedFiles => 60(:19). The floor of 60 raises the base default of 1 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:22); the comment sizes it against roughly 77 razor files across the three module UIs and the UI hosts (LocalizedTextConventionTests.cs:12-:13). The inherited fact isUserVisibleText_IsLocalized(LocalizedTextConventionTestsBase.cs:32), which resolves the repo root fromMap.RepoTokenand scansSource/(:34-:38); the base'sAllowedFileshook (:29) is left empty, so ADC exempts nothing at file granularity. The scan covers snackbar messages, pageTitleproperties,<PageTitle>markup, breadcrumb labels, NavItem titles (which must carry aTitleResource), and the shared delete confirmation'sEntityTypenoun (LocalizedTextConventionTests.cs:6-:9).- The
EntityTypecheck lives in the shared rule, not in ADC. The delete confirmation rendersEntityTypein its dialog title and prompt, so a literal noun would show an English word inside a Spanish sentence.ArchitectureRules.UserVisibleTextIsLocalizedtracks aDeleteConfirmationstart tag across lines until it closes and reports any line matching theLiteralEntityTypegenerated regex asfile:line passes a literal EntityType to DeleteConfirmation(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:133-:146). That regex flags a quoted value not starting with@or an unquoted bare word, so a razor expression such asEntityType="@EntityName"passes (:38-:41, with a 1000 ms match timeout). Omitting the attribute is also fine, because the component then falls back to its own localized generic noun.
- The
MicroserviceExtractionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the single-fact guard that transport never leaks into the core layers, so a module behaves identically in-process or extracted.
- Depends on - MicroserviceExtractionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). One inherited fact,CoreLayers_ShouldNotDependOn_Transport(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:13).[Rubric §7 - Microservices Readiness]assesses whether extraction is a configuration change or a rewrite. ADC is the repo where this rule has already been cashed in: all four modules run as separate service hosts behind a YARP gateway (ADR-007, ADR-008), and the rule is what keeps the core layers clean enough for the next one.
ModuleIsolationTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3· Level 13 · class (public, sealed)
- What it is - the guard that Identity, Conference, Engagement, and Notification do not reach into each other: module Domains, Applications, Infrastructures, and APIs are each isolated from their siblings, neither Domain nor Application may reach another module's Infrastructure, and a seventh rule closes every remaining internal-layer pair.
- Depends on - ModuleIsolationTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Seven inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13,:16,:19,:22,:25,:28,:36). The last,ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers(:36), exists to close the coverage the first six leave open: every remaining cross-module internal-layer pair (Domain to another Application or Api, Application to another Domain or Api, Infrastructure and Api to anything but their own module), with UI excluded on purpose (:30-:34). The targets are computed by the map: for each module and layer it derives the other modules' root namespaces (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:69-:72), which is why adding a module to the map immediately expands what every other module is forbidden to touch.[Rubric §7 - Microservices Readiness]and[Rubric §5 - Vertical Slice]: cross-module collaboration in ADC goes through interfaces satisfied by gRPC clients, and this rule is what stops a direct reference from quietly becoming the cheaper option.
NamingConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the ten-fact naming and sealing guard: handler, command, query, validator, DTO, specification, repository, and EF configuration suffixes, plus domain events sealed in a
*.DomainEventsnamespace and invariant classes static. - Depends on - NamingConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Ten inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13through:40).[Rubric §15 - Best Practices & Code Quality], and more practically[Rubric §15 - Best Practices & Code Quality]: several framework mechanisms (the Scrutor handler scan, the DTO/mapper registration, the decorator wrapping) find their targets by convention, so a naming slip is not cosmetic, it silently un-registers a type.
PiiConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Privacy·MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs:18· Level 13 · class (public, sealed)
- What it is - the privacy structural guard, in both directions (ADR-005): every domain entity declaring a PiiAttribute-marked property must implement IAnonymizable, so an entity that holds personal data always has an erasure path; and whatever
Anonymize()overwrites must itself carry[Pii], so the audit trail redacts it instead of keeping the pre-erasure value as anOldValue. - Depends on - PiiConventionTestsBase and AdcArchitectureMap.
- Walkthrough - two members:
Map(:44) andAnonymizableSamples(:46), which overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:31) with a privateSamplesdictionary (:20-:42). Three inherited facts:EntitiesWithPiiProperties_ShouldImplement_IAnonymizable(PiiConventionTestsBase.cs:34),EveryAnonymizableDomainEntity_IsCoveredBySample(:37), andPropertiesOverwrittenByAnonymize_MustCarryPii(:43). The reverse rule is behavioral, so it only sees what a sample actually populates. TheIdentity.Domain.Users.Usersample is an external account (CreateExternal, which setsLoginProviderandProviderKey) with every device field and an avatar filled in, so each field the erasure nulls starts at a value it has to move (PiiConventionTests.cs:23-:30). TheConference.Domain.Speakers.Speakersample fills every profile field and links a user (:31-:41), so anAnonymize()that moved anything beyond the email would fail the reverse rule on a public field that carries no[Pii].[Rubric §30 - Compliance/Privacy/Data Governance]assesses whether privacy obligations are structural rather than procedural: soft delete is the default everywhere in MMCA, so erasure has to be an explicit, tested capability (ADR-005). Unlike its counterpart in MMCA.Common, this instance is non-vacuous: ADC's Identity domain holds real attendee data.
PiiInventoryTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Privacy·MMCA.ADC.Architecture.Tests/Privacy/PiiInventoryTests.cs:15· Level 13 · class (public, sealed)
- What it is - the two-way personal-data inventory gate for ADC: the
[Pii]markers in the domain code and the inventory table inPRIVACY.mdmust describe the same set of members (ADR-005). - Depends on - PiiInventoryTestsBase (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:33), AdcArchitectureMap, and ArchitectureMapBase (statically, for the repo root). Companion to PiiConventionTests, which checks the erasure path rather than the documentation. - Walkthrough - two members, both supplying the base's abstract inputs.
Assemblies(PiiInventoryTests.cs:17) is every Domain-layer assembly the map knows,new AdcArchitectureMap().OfLayer(Layer.Domain).PrivacyPolicyPath(:19-:20) isPRIVACY.mdat the repo root, found viaFindRepoRoot("MMCA.ADC.slnx"). Two inherited facts do the work.PiiMembers_ShouldHave_CompleteInventoryRows(PiiInventoryTestsBase.cs:42) requires every[Pii]property to have an inventory row with thePurpose,Lawful basisandRetentioncells filled.InventoryRows_ShouldMapTo_LivePiiMembers(:46) is the reverse, so the policy cannot keep promising things about data the code no longer marks. The table is any Markdown table inPRIVACY.mdwhose header holdsMember,Purpose,Lawful basisandRetention, with the member writtenType.Property(PiiInventoryTestsBase.cs:13-:25). If the file is missing, the base fails with a message naming the path (:51-:52).[Rubric §30 - Compliance/Privacy/Data Governance].
QueryHandlerReadRepositoryTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9· Level 13 · class (public, sealed)
- What it is - the read-repository guard: every
IQueryHandlerin ADC's module Application assemblies must takeIUnitOfWork.GetReadRepositoryand never the write repository, since a query handler reads and the write repository hands it the mutation surface (add, remove, tracked reads) meant only for aggregate roots (MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:3-:7). - Depends on - QueryHandlerReadRepositoryTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:11). One inherited fact,QueryHandlers_ShouldUseReadRepositories(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:24-:26), delegating toArchitectureRules.QueryHandlersUseReadRepositories(Map, AllowedHandlers), which reads the IL of everyIQueryHandler<,>implementation in the map's Application assemblies, async and lambda bodies included (QueryHandlerReadRepositoryTestsBase.cs:6-:8). ADC adopts it with an empty allowlist:AllowedHandlerskeeps the base's empty default, because every query handler already reads through the narrow interfaces (QueryHandlerReadRepositoryTests.cs:6-:7;QueryHandlerReadRepositoryTestsBase.cs:22).[Rubric §8 - Data Architecture]assesses this same read/write separation directly; ADC's read-only data access convention (handler locals takeIEntityQuerier<,>/IEntityReader<,>fromIUnitOfWork.GetReadRepository<>()) is what this rule verifies structurally rather than by review. - Why it's built this way - the base's own doc comment states the adoption path for a repo with existing offenders: subclass, run once, and either switch each reported handler to
GetReadRepositoryor move it intoAllowedHandlerswith a comment saying why it needs the write surface (QueryHandlerReadRepositoryTestsBase.cs:9-:13). ADC needed neither: the empty allowlist means the rule already passed clean on adoption. - Where it's used - MMCA.Common carries the base's own self-test in
MMCA.Common.Architecture.Tests, which runs the same rule over Common's own Application layer and also over compiled fixtures inReadRepositoryFixtures, proving the IL-reading rule catches a write-repository call sitting after anawait, inside the compiler's state machine (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:8-:12,:23-:26).
RawQueryableConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11· Level 13 · class (public, sealed)
- What it is - the rule that Application-layer code must not use the repository's raw
IQueryablesurfaces (Table/TableNoTracking*), carrying a seven-file allowlist that pins ADC's existing deliberate uses (MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:3-:10,:33-:49). - Depends on - RawQueryableConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (statically, for the repo root at
:28), andSystem.IO.Path. - Concept introduced, the adoption ratchet. A convention introduced into a codebase that already violates it in seven places has two bad options: fail the build on day one, or exempt the whole layer. The ratchet is the third: enumerate the existing violations explicitly so that new code is what the rule blocks, then shrink the list over time. The comment states the discipline directly, "Shrink it over time; never grow it without the same scrutiny" (
:8-:9).[Rubric §8 - Data Architecture]assesses how query access is layered;[Rubric §7 - Microservices Readiness]is the stated motivation: a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary (:5-:6). - Walkthrough - three members.
Map(:13).ApplicationSourceDirectories()(:21-:30) overrides the base's virtual enumeration (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:45) by yielding everything the base produces (:23-:26) and then appending one more path:Source/Modules/Notification/MMCA.ADC.Notification.Application, resolved fromFindRepoRoot("MMCA.ADC.slnx")(:28-:29).AllowedFiles(:33-:49) overrides the base's empty default (RawQueryableConventionTestsBase.cs:38) with seven file names, each grouped under a comment saying why it is exempt: the Engagement live layer's conference-day hot-path aggregations, which need GROUP BY and COUNT shapes the focused repository surface cannot express (LivePollResultsBuilder, SessionQuestionViewBuilder, GetModerationQueueHandler, GetSessionQuestionsHandler,:35-:40); the Engagement bookmark count and page projection (BookmarkCountService, GetUserBookmarksHandler,:42-:45); and Notification's GDPR export joins (UserNotificationExportService,:47-:48). The two Identity entries (the user-list paging handler and the administration-roster service) are no longer allowlisted. Every entry is annotated as intra-module, which is the actual test for whether an exemption is safe: the queries never cross a module boundary, so they would travel with the module if it moved.- The inherited fact is
ApplicationLayer_DoesNotUseRawQueryableSurfaces(RawQueryableConventionTestsBase.cs:61), a textual scan rather than an assembly scan, which is why it needs directories rather than the map's assemblies; it also asserts the directory set is non-empty first (:65-:66).
- Caveats / not-in-source -
AllowedFilesmatches by file name, not by path, so two files with the same name in different modules would both be exempted. The<remarks>on the directory override still says the thin Notification module "is not a mapped module" (:16-:20), but AdcArchitectureMap now registers it (AdcArchitectureMap.cs:52-:54) and the base derives its directories fromMap.ModuleNames(RawQueryableConventionTestsBase.cs:50-:57), so the appended path is a duplicate of one the base already yields and the comment no longer matches the map. Nothing here enforces the "shrink it over time" discipline the class comment asks for.
RawSqlConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs:14· Level 13 · class (public, sealed)
- What it is - the companion rule to RawQueryableConventionTests: module code must reach SQL only through the parameterizing APIs, banning
FromSqlRaw,SqlQueryRaw,ExecuteSqlRawandExecuteSqlRawAsync(each takes a plain string, so an interpolated value silently loses its parameters and the injection becomes a code-review question) while leaving the interpolated siblings andIRawSqlQueryExecutorallowed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:6-:19). - Depends on - RawSqlConventionTestsBase, AdcArchitectureMap, ArchitectureMapBase (statically, for the repo root at
RawSqlConventionTests.cs:31), andSystem.IO.Path. - Walkthrough - two members.
Map(:16), andScannedSourceDirectories()(:24-:33), overriding the base's virtual enumeration (RawSqlConventionTestsBase.cs:48) exactly as RawQueryableConventionTests'ApplicationSourceDirectories()does: it yields everything the base produces (:26-:29) and appendsSource/Modules/Notification/MMCA.ADC.Notification.Application, resolved fromFindRepoRoot("MMCA.ADC.slnx")(:31-:32), for the same reason (the thin Notification module is not a mapped module). Unlike its sibling, ADC declares noAllowedFilesoverride, so the base's empty default (RawSqlConventionTestsBase.cs:39) stands: no raw-SQL exemption exists anywhere in ADC today. The inherited fact isModuleCode_UsesParameterizedSqlOnly(RawSqlConventionTestsBase.cs:65-:66), the same textual scan, over.csfiles underScannedSourceDirectories, that its sibling uses and for the same reason: this package carries no IL or Roslyn dependency, so reflection cannot see member usage inside method bodies (RawSqlConventionTestsBase.cs:21-:28).[Rubric §11 - Security]is the direct read (a raw string reaching SQL is the injection surface);[Rubric §8 - Data Architecture]covers the same discipline as its sibling.
ServiceContractPurityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9· Level 13 · class (public, sealed)
- What it is - the purity rule for the published gRPC wire surface: a type marked ServiceContractAttribute must not depend on the producing service's Domain, Application, or Infrastructure (
MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:3-:8). Map-only shape (ConcurrencyConventionTests). - Depends on - ServiceContractPurityTestsBase and AdcArchitectureMap.
- Concept introduced, the attribute-driven ratchet. The other purity rules in this unit iterate layers. This one cannot: no repo registers
Layer.Contractsin its map today, so a layer-iterating rule would pass vacuously forever without anyone noticing (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-:11). Instead it scans every assembly the map registers for types carrying the marker, wherever they live, and enforces the invariant from the first marked type onward (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:21-:24).[Rubric §7 - Microservices Readiness]assesses whether a service can be consumed without its internals: a contract that leaks a domain entity or a persistence type forces every consumer to take the producer as a package dependency, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:14-:18).[Rubric §9 - API & Contract Design]covers the same boundary from the contract side, and ContractImplementationTests is the twin guarding it from the other direction. - Walkthrough - one member,
Map(:11), implementing the base's abstract property (ServiceContractPurityTestsBase.cs:22). One inherited fact,ServiceContracts_ShouldNotDependOn_ServiceInternals(:25), delegating toArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map)(ArchitectureRules.Contracts.cs:32-:54). The rule computes the forbidden internal namespaces from the map, returns immediately if that set is empty (:34-:38), and otherwise runs a NetArchTest query per mapped assembly against the types carrying the marker (:40-:53). The marker is matched by full name,MMCA.Common.Shared.Abstractions.ServiceContractAttribute(:10-:11), keeping the rule library free of a framework reference. - Why it's built this way - the base adds a second consequence that reads as a design statement (
ArchitectureRules.Contracts.cs:26-:29): a marked type living inside a Domain, Application, or Infrastructure assembly fails by construction, and that is the intent, because a published contract belongs in a*.Contractsor Shared assembly rather than inside the service it describes. - Where it's used - the rule is live in ADC, not latent. Six
[ServiceContract]interfaces are declared in mapped module Shared assemblies today:IAttendeeQueryService(MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs:10),IEventLiveValidationService(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/Live/IEventLiveValidationService.cs:12),ISessionBookmarkValidationService(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Sessions/ISessionBookmarkValidationService.cs:10),IBookmarkCountService(MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs:10),IUserEngagementExportService(MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs:13), andIUserNotificationExportService(MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs:13). All six sit in Shared assemblies the map registers, so the rule inspects them on every run. - Caveats / not-in-source - the
*.Contractsprojects that hold the generated gRPC clients and adapters are not in AdcArchitectureMap, so any[ServiceContract]type declared there rather than in Shared would be outside this scan.
HandlerResultConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8· Level 13 · class (public, sealed)
- What it is - the gate that turns a runtime constraint into a build-time one: every ADC command and query handler's
TResultmust be Result orResult<T>(MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:3-:7). - Depends on - HandlerResultConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Concept introduced - shifting a failure left. The decorator pipeline can short-circuit (a feature flag off, an authorization denial, a validation failure, a cache hit), and to do that it must manufacture a failed result of the handler's
TResult; the comment names the mechanism,ResultFailureFactory(:5-:6). A handler returning a bare DTO therefore compiles and registers cleanly and only explodes the first time a short-circuit fires in production.[Rubric §6 - CQRS & Event-Driven]and[Rubric §14 - Testability]: an invariant the type system cannot express is exactly what a fitness function is for. - Walkthrough - one member,
Map(:10). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:21,:24,:27): the Application layers declare at least one handler (a non-vacuity check), command handlers return result types, and query handlers do too. Opt-in from v1.120.0 (HandlerResultConventionTests.cs:4).
IdempotencyConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3· Level 13 · class (public, sealed)
- What it is - the rule that every POST action in ADC's API layer states, in code, whether a retried request replays the original response or deliberately does not. Map-only shape (ConcurrencyConventionTests): the body is one line (
MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:5). - Depends on - IdempotencyConventionTestsBase and AdcArchitectureMap, and, by attribute name, IdempotentAttribute and NonIdempotentAttribute.
- Concept introduced, forcing a decision rather than a default. POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework's answer is the
Idempotency-Keyfilter, and it costs an existing client nothing, because the filter no-ops for a request that carries no key header. That makes the only failure mode worth gating an omission nobody noticed: an action that should replay but silently does not. The rule therefore does not demand[Idempotent]; it demands that the author write down which of the two applies, with[NonIdempotent("why")]carrying its own justification string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:14-:18,:22-:28,:58-:60).[Rubric §9 - API & Contract Design]assesses retry semantics as part of the contract;[Rubric §29 - Resilience & Business Continuity]: a retry that double-writes is precisely the failure a resilience policy creates when the endpoint has not thought about it. - Walkthrough - one member,
Map(:5), implementing the base's abstract property (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:12). One inherited fact,PostActions_ShouldDeclare_IdempotencyIntent(:15), delegating toArchitectureRules.PostActionsDeclareIdempotencyIntent(Map)(ArchitectureRules.Idempotency.cs:44-:61), which walks the concrete controllers in everyLayer.Apiassembly the map declares (:48-:54). Attributes are read withinherit: trueand abstract controller types are skipped, so an ADC controller that inherits a POST action fromAuthControllerBaseorAggregateRootEntityControllerBasealready satisfies the rule through that base rather than needing its own attribute (:31-:35). - Why it's built this way - the base states the subclassing rule: derive it in a repo whose map declares an
Apilayer, and a repo with no API layer simply does not subclass (IdempotencyConventionTestsBase.cs:6-:8). ADC declares an Api layer for all four mapped modules, Notification included (AdcArchitectureMap.cs:54), so the gate is live across the whole REST surface. - Caveats / not-in-source - detection is by attribute type name, keeping the rule library free of an ASP.NET reference, and only
[HttpPost]is recognised: an action routed through[AcceptVerbs("POST")]or a conventional route is out of scope (ArchitectureRules.Idempotency.cs:38-:41).
ImmutabilityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3· Level 13 · class (public, sealed)
- What it is - the immutability guard across five categories of type: DTOs, commands and queries, domain events, integration events, and value objects (the last also required to be sealed and to live in Shared).
- Depends on - ImmutabilityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Five inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13,:16,:19,:22,:25).[Rubric §15 - Best Practices & Code Quality]assesses whether the codebase holds its stated conventions;required/initimmutability is a workspace-wide convention, and an event whose properties can be mutated after publication is a correctness hazard, not a style preference.
IntegrationEventContractTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Contracts·MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3· Level 13 · class (public, sealed)
- What it is - the frozen wire contract for ADC's cross-service asynchronous API. It commits a seven-line snapshot of every integration event's wire name and property shape, and the build fails if the live contract differs (
MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:9-:20). - Depends on - IntegrationEventContractTestsBase and AdcArchitectureMap.
- Concept introduced, the approval snapshot. The rules above check shape rules; this one checks identity. A consumer in another service deserializes by shape, so a renamed, removed, or retyped property (or a brand-new event shipped without a consumer) breaks the contract at runtime with no compile error anywhere. The base rebuilds the live contract from the map (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:38) and reports every observable difference against the committed list (:63-:78): a missing event, a new event, and per event a missing, extra, or retyped member. So any change surfaces as a diff in this file that a reviewer must consciously accept.[Rubric §9 - API & Contract Design]assesses contract governance, and the asynchronous contract is as much an API as the REST surface;[Rubric §7 - Microservices Readiness]: with all four ADC modules already running as separate services, this list is the actual coupling between them. Its synchronous twin is ProtoContractTests. - Walkthrough - two members.
Map(:5), andExpectedContract(:9-:23), a collection expression of seven strings inKey { Prop:Type, ... }form, where the key is the event's[EventName]wire identity (Conference.EventFeedbackSubmitted.v1,Identity.UserRegistered.v1) and falls back to the full type name only for an event that declares none (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:98-:89): EventFeedbackSubmitted and SessionFeedbackSubmitted from Conference, SpeakerLinkedToUser and SpeakerUnlinkedFromUser from Conference (the pair Identity consumes to set and clear the linked-speaker reference on the user), AttendeeCheckedIn from Engagement, and UserDeleted plus UserRegistered from Identity. The builder lists properties sorted by name with generic arguments spelled out (SessionId:Nullable<Int32>,ArchitectureRules.Events.cs:80-:75,:98), but the base compares each event's members as a set, not a sequence, because JSON carries no member order and a reordered declaration changes nothing a consumer can observe (IntegrationEventContractTestsBase.cs:11-:15). The inherited fact isIntegrationEventContracts_ShouldMatch_TheFrozenSnapshot(IntegrationEventContractTestsBase.cs:36). - Why it's built this way - the comment states the rule of engagement (
:7-:8): update the snapshot deliberately, and version the event or coordinate the consumer rollout in the same commit. Keying each line on the[EventName]value (for example[EventName("Engagement.AttendeeCheckedIn.v1")],MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/CheckIns/IntegrationEvents/AttendeeCheckedIn.cs:23) means a CLR rename or namespace move does not churn the snapshot, exactly as it does not churn the wire (ArchitectureRules.Events.cs:93-:81); the.v1suffix is where a breaking change gets versioned. TheAttendeeCheckedInentry carries its own inline justification (:15-:16):SponsorIdis additive, optional, defaults to null, and is declared last precisely so a payload written before the sponsor scope existed still deserializes (confirmed in the event itself,MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/CheckIns/IntegrationEvents/AttendeeCheckedIn.cs:22,:31).UserRegisterednow carries the same pattern (:19-:21):EmailVerifiedis additive and optional, defaulting tofalse, so a payload written before the flag existed still deserializes, and it deserializes to the value that links nothing, since the Conference consumer auto-links a speaker only on a verified provider email. - Caveats / not-in-source - the snapshot proves that the shape has not changed, not that any consumer actually handles it. Consumer-side behavior is exercised by the cross-service Testcontainers tier, not here.
StronglyTypedIdTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/StronglyTypedIdTests.cs:9· Level 13 · class (public, sealed)
- What it is - the strongly typed identifier fitness function for ADC (ADR-115): any wrapper type that opts into
IStronglyTypedId<TSelf, TValue>must be a readonly record struct, held to the shape the framework's converters, comparer, and route binding are written against. - Depends on - StronglyTypedIdTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:11). One inherited fact,StronglyTypedIds_ShouldBe_ReadonlyRecordStructs(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:14-:15).[Rubric §8 - Data Architecture]. - Caveats / not-in-source - the base is vacuously satisfied in a repo that has not opted in, which is ADC today: the primitive identifier aliases (ADR-048/ADR-085) remain the default identifier model here, so this instance passes with nothing to check, and exists so the FIRST wrapper ADC declares is held to the shape (
StronglyTypedIdTestsBase.cs:4-:7).
UIArchitectureConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10· Level 13 · class (public, sealed)
- What it is - the container/presentational split enforced mechanically: every code-behind under
Source/(module UI and UI hosts alike) stays within the 400-line convention cap, and inline@codeblocks stay small (MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:3-:9). - Depends on - UIArchitectureConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Concept introduced - a size cap as a proxy for a structural property. Nothing can test "this component separates orchestration from presentation", but a code-behind that has grown past 400 lines has almost certainly stopped doing so. The comment records the payoff: the gate subsumes tech-debt item TD-13, because the oversized Conference dashboards were split to conform when it landed (
:7-:8). That is the ratchet working in the other direction from RawQueryableConventionTests: instead of pinning the violations, the violations were fixed.[Rubric §18 - UI Architecture]assesses front-end composition and is the rule's own stated target. - Walkthrough - one member,
Map(:12). Two inherited facts,CodeBehinds_StayWithinTheLineCapandRazorFiles_KeepInlineCodeBlocksSmall(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:44,:63). All four tuning hooks stay at their base defaults:MaxCodeBehindLines400 (:22),MaxInlineCodeLines120 (:29),MinimumCodeBehindFiles1 (:35), and an emptyExcludedPathFragments(:41), so ADC excludes nothing from the scan.
DeleteBehaviorConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:29· Level 15 · class (public, sealed)
- What it is - the ADC-side delete-behavior fitness function: every foreign key that cascades across ADC's four finalized service models must carry an explicit
MMCA:DeleteBehaviorSource = Explicitannotation, and every foreign key the framework'sRestrictDeleteByDefaultConventionstampedConventionmust restrict rather than cascade (:8-:11). - Depends on - DeleteBehaviorConventionTestsBase for the shared assertion logic and the
Modelextension point, ArchitectureRules for the two rule methods it invokes directly (CascadingForeignKeysAreExplicitlyOptedIn,ConventionForeignKeysRestrictDeletes), and the nested ServiceModels factory for every model it checks. - Concept introduced - cross-reference DeleteBehaviorConventionTestsBase, which is where the base assertion mechanism is taught; this type is the ADC-specific instantiation of that base over four databases instead of one.
[Rubric §8 - Data Architecture]applies: an unannotated cascade is exactly the kind of silent-fan-out delete this gate exists to catch.[Rubric §14 - Testability]also applies: the whole check runs against an in-memory-built model, so it stays in the fast tier instead of needing a live database. - Walkthrough -
Model(:31) overrides the base class's single-model extension point withServiceModels.Conference(), so every[Fact]inherited fromDeleteBehaviorConventionTestsBaseruns against Conference by default (it "owns the aggregates with the most relationships",:24-:26). The other three databases get their own explicit fact pairs, oneCascadingDeletes_AreExplicitlyOptedInand oneConventionDeletes_AreRestrictedper service, each wrapping the matchingArchitectureRulescall inFluentActions.Invoking(...).Should().NotThrow(): Identity (:39-:47), Engagement (:49-:57), Notification (:59-:67). The nestedServiceModelsclass (:70-:129) supplies every model these facts and the inherited base facts check. - Why it's built this way - the class doc comment records the deliberate scope: the migrations projects themselves are NOT referenced here, because their generated migration and snapshot code would otherwise land in the unit-tier coverage denominator (
:18-:19); insteadServiceModelsrebuilds the same finalized model the migrations projects' own design-time factories build, so the two must be kept in step by hand. See ADR-119 (restrict-delete-by-default) for the policy this gate enforces. - Where it's used - a standalone architecture-tier test class; nothing else in the codebase references it.
- Caveats / not-in-source - the doc comment flags that these framework-table flags must stay in step with each service's migrations design-time factory and its
appsettings.json, or the gate and the scaffolded migrations silently disagree (:16-:17); nothing in source enforces that agreement mechanically.
SliceCohesionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Cqrs·MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8· Level 13 · class (public, sealed)
- What it is - the vertical-slice cohesion rule: every module's
Application/{Aggregate}/UseCases/{Operation}/slice keeps its command or query, its handler, and its validator in one namespace, and the build fails if a handler is stranded from its contract (MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:3-:7). - Depends on - SliceCohesionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:10). Two inherited facts,Handlers_ShouldBeCoLocatedWith_TheirContractsandValidators_ShouldBeCoLocatedWith_TheirContracts(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:15,:19).[Rubric §5 - Vertical Slice]assesses whether a feature is one navigable unit; the rule turns the folder convention into something the build can check, so the slice does not erode into a layer-per-type layout one refactoring at a time.
SoftDeleteEnforcementTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15· Level 13 · class (public, sealed)
- What it is - the ADC soft-delete enforcement guard (ADR-005): ADC deletes by setting
IsDeleted = true, so EF Core's row-erasing members are banned everywhere except a reviewed allowlist of purge types (MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:3-:7). - Depends on - SoftDeleteEnforcementTestsBase and AdcArchitectureMap. Unlike the map-only instances it also overrides the base's allowlist hook.
- Concept introduced - an allowlist that names types, not a namespace. Every allowlisted type lives in MMCA.Common, and no ADC module, service or host erases a row of its own; the list still names each framework type individually rather than exempting
MMCA.Commonwholesale, so a framework type that starts erasing rows in a later release fails this gate and gets reviewed (:9-:12).[Rubric §8 - Data Architecture]applies to the delete policy itself, and[Rubric §11 - Security]to the retention entries, where erasing is the privacy requirement rather than the defect. - Walkthrough - two members.
Map(:17) supplies AdcArchitectureMap.AllowedHardDeleteTypes(:20-:56) overrides the base's empty default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:27) with seven fully qualified type names, each with its reason in a comment (the base accepts full type names or namespace prefixes,SoftDeleteEnforcementTestsBase.cs:21-:26, and ADC uses full names only):EFRepository`2as the framework's set-based delete escape hatch, whose one ADC caller (SessionScoringRunner) replaces derived AI scores before rescoring (:22-:26);OutboxCleanupServicefor outbox and inbox retention (:28-:31);AuditTrailCleanupJobfor audit-trail retention (:33-:35);RefreshSessionCleanupServicefor refresh-session rows, which carry noIsDeletedflag and hold device data (:37-:41);InternalCommandCleanupServiceandInternalCommandAdministrationfor scheduled-command retention, on a timer and on demand (:43-:50); andEFPermissionGrantStore, because a revoked grant must erase its row (:52-:55). One inherited fact,HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes, which hands the map and the list to ArchitectureRules.HardDeletesOnlyInAllowedTypes(SoftDeleteEnforcementTestsBase.cs:29-:31). - Caveats / not-in-source - the allowlist is matched by type name string, so a rename of any listed framework type turns this gate red until the entry is updated; that coupling is the intended review trigger, not a defect.
SpecificationConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8· Level 13 · class (public, sealed)
- What it is - the cross-source specification guard: no specification may filter by navigating to another entity, because such a filter would not translate if that entity later moved to a different data source. The stated alternative is CrossSourceSpecification (
MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:3-:7). - Depends on - SpecificationConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Concept introduced - a forward safeguard for a capability that is not currently exercised. Every ADC entity routes to SQL Server today: the Conference Session-to-Cosmos and Room-to-SQLite polyglot trial was reverted, but the framework extension points were kept (ADR-018), so ADC opts into the rule anyway (
:3-:5). This is worth noticing as a deliberate choice: the rule costs nothing while the repo is single-engine and prevents a class of query from being written that would have to be unwritten later. - Walkthrough - one member,
Map(:10). One inherited fact,Specifications_ShouldNotNavigate_ToOtherEntities(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16).[Rubric §8 - Data Architecture].
StateManagementConventionTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Ui·MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9· Level 13 · class (public, sealed)
- What it is - the Blazor state-ownership guard: the Identity, Conference, and Engagement UI assemblies carry no mutable static state, and stateful UI services stay scoped (
MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:3-:8). - Depends on - StateManagementConventionTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Concept introduced - why this is a correctness rule and not a style rule. Under Blazor Server every user gets a circuit inside one process, so a mutable static member is shared across every connected attendee, and a singleton-registered stateful service is the same defect wearing a DI hat: one user's selection becomes everyone's. The comment says exactly this (
:6-:7).[Rubric §19 - State Management]assesses how client state is scoped and owned;[Rubric §11 - Security]is the sharper edge, since cross-circuit leakage of user state is a data exposure, not just a bug. - Walkthrough - one member,
Map(:11). Two inherited facts,UiAssemblies_CarryNoMutableStaticStateandUiProjects_RegisterStatefulServicesScoped(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:29,:66). The base'sAllowedStaticMembershook (:25) is left at its empty default, so ADC's module UIs have zero static-state allowances. Notification declares noLayer.Uiassembly in the map, so the scan covers the three module UIs only.
SharedLayerTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Layering·MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3· Level 13 · class (public, sealed)
- What it is - the guard on the Shared layer, the one layer other modules are allowed to reference: a module's Shared project must not depend on that module's own internal layers, must not reach sibling modules, and must stay free of EF Core.
- Depends on - SharedLayerTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:5). Three inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12,:15,:18).[Rubric §7 - Microservices Readiness]and[Rubric §9 - API & Contract Design]: Shared holds the DTOs, requests, integration events, and (in ADC) the six[ServiceContract]interfaces that cross a module boundary, so a dependency from Shared into Domain or Infrastructure would drag the module's internals along with its contract and make extraction impossible.
SubstitutabilityTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Governance·MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs:9· Level 13 · class (public, sealed)
- What it is - the Liskov substitutability guard: no override of a base virtual or abstract member, and no interface member implementation, in the ADC module assemblies throws
NotSupportedExceptionorNotImplementedException, so a subtype never refuses at run time an operation its base type or interface promises (MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs:3-:8). - Depends on - SubstitutabilityTestsBase and AdcArchitectureMap. Map-only shape (ConcurrencyConventionTests).
- Walkthrough - one member,
Map(:11). One inherited fact,OverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:34). The base reads IL, so only an exception constructed in place and thrown is judged, and a method that overrides and implements nothing may throw freely (SubstitutabilityTestsBase.cs:11-:13). The base'sExcludedMembershook (:25-:30) is left at its empty default, so ADC carries no reviewed refusals.[Rubric §3 - SOLID Principles](Liskov) assesses whether a subtype honors its contract; this turns that into a build failure.
SoftDeleteEnforcementTests
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests·MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15· Level 13 · class (public, sealed)
- What it is - the rule that EF Core's row-erasing members are banned outside seven named framework types (
MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:20-:56). It is the counterpart to CascadeSoftDeleteConventionTests: that one checks the cascade is a soft delete, this one checks nothing erases in the first place. - Depends on - SoftDeleteEnforcementTestsBase and AdcArchitectureMap.
- Concept introduced, an allowlist that names types instead of a namespace. The base bans
DbSet.Remove/RemoveRange,DbContext.Remove/RemoveRange, andExecuteDelete/ExecuteDeleteAsyncoutside the purge and erasure types the repo lists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:4-:9). ADC's<para>explains the shape of its own list (SoftDeleteEnforcementTests.cs:8-:13): every allowlisted type is in MMCA.Common, because no ADC module, service, or host erases a row of its own, and the list names those framework types individually rather than exempting theMMCA.Commonnamespace wholesale, so a future framework type that starts erasing rows still fails here and gets reviewed.[Rubric §30 - Compliance/Privacy/Data Governance]and[Rubric §8 - Data Architecture]: soft delete is what keeps a deleted row auditable, restorable, and accounted for under an erasure request (ADR-005). - Walkthrough - two members.
Map(:17), andAllowedHardDeleteTypes(:20-:56), which overrides the base's empty default (SoftDeleteEnforcementTestsBase.cs:27) with seven entries, each carrying its own justification.- EFRepository<TEntity, TIdentifierType> (
:26), the framework's set-based delete escape hatch. Erasing is the caller's explicit ask there, and the comment names the one ADC caller,SessionScoringRunner, which replaces a session's AI scores before rescoring, so it is deleting derived rows it is about to rewrite rather than user data (an AI score carries no audit or erasure obligation of its own) (:22-:25). - OutboxCleanupService (
:31, listed by its full nameMMCA.Common.Infrastructure.Persistence.Outbox.Administration.OutboxCleanupService), outbox and inbox retention: delivery plumbing with a bounded lifetime, where the dead-letter sweep is the retention policy and soft-deleting would grow the table the job exists to bound (:28-:30). - AuditTrailCleanupJob (
:35), audit-trail retention, with the sharpest line in the file: erasing past the retention window is the requirement, because keeping an audit row forever is the privacy defect and not the safeguard (:33-:34). - RefreshSessionCleanupService (
:41), refresh-session retention. The reasoning is worth reading in full (:37-:40): a session row is framework bookkeeping rather than an aggregate, it carries noIsDeletedflag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it. InternalCommandCleanupService(:46,MMCA.Common.Infrastructure.Persistence.InternalCommands.Administration.InternalCommandCleanupService), internal-command retention: a scheduled-command row is deferred-work plumbing on the outbox's design, and purging processed and dead-lettered rows past the retention window IS the policy, so soft-deleting them would grow the table the cleanup exists to bound (:43-:45).InternalCommandAdministration(:50,MMCA.Common.Infrastructure.Persistence.InternalCommands.Administration.InternalCommandAdministration), the internal-command operator surface: the administration purge is the same retention action taken on demand rather than on a timer, over the same non-aggregate plumbing rows (:48-:49).EFPermissionGrantStore(:55,MMCA.Common.Infrastructure.Persistence.Auth.EFPermissionGrantStore), revoked permission grants: revoking a stored grant must erase the row, since a flagged row would leave a capability record behind and the compiled registry, not the table, is the audit of what a role can do (ADR-005) (:52-:54).- The inherited fact is
HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes(SoftDeleteEnforcementTestsBase.cs:30).
- EFRepository<TEntity, TIdentifierType> (
- Why it's built this way - the entries are a good worked example of how MMCA reasons about erasure: most of them erase because privacy requires it, not despite it. The base frames the list the same way as the other ratchets, as a reviewed inventory of every place that does not soft-delete rather than a blanket exemption (
SoftDeleteEnforcementTestsBase.cs:11-:15). - Caveats / not-in-source - entries are full names including the generic-arity suffix (
EFRepository`2), so an arity change would drop the exemption and turn the rule red, which is the safe direction.
ServiceModels
MMCA.ADC.Architecture.Tests ·
MMCA.ADC.Architecture.Tests.Domain·MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:70· Level 14 · class (private, static, nested in DeleteBehaviorConventionTests)
- What it is - a private static factory nested inside
DeleteBehaviorConventionTests.csthat builds each of ADC's four service databases' finalized EF model offline, one method per service (Identity,Conference,Engagement,Notification,:76-:98), so the delete-behavior fitness functions can assert against the real scaffolded model without ever opening a connection. - Depends on - DesignTimeDbContextHelper (
.CreateSqlServer,:107), DataSourceEntrySettings (:114-:118), and DefaultEntityConfigurationAssemblyProvider as Notification's configuration assembly (:96, since Notification's EF configurations ship insideMMCA.Common.Infrastructurerather than its own module assembly). Each of Identity/Conference/Engagement instead pointsBuildat its own module'sInfrastructure.AssemblyReferencemarker type (:79,:85,:88). - Concept introduced - building a design-time EF model in-process as a fitness-function fixture. This mirrors exactly what
dotnet ef migrations addbuilds when a migration is scaffolded (sameDesignTimeDbContextHelperentry point, same single-named-DataSourcecollapse ontoDefault), which is the point: ifServiceModelsand a service's real migrations design-time factory ever disagree, the scaffolded migrations and this gate stop agreeing too. The class is deliberately private and file-scoped rather than shared, so each architecture-test file owns its own snapshot of "how do I build service X's model" without a shared dependency that could silently drift for one caller and not another. - Walkthrough -
DesignTimeConnection(:74) is a placeholder connection string; it is never opened because the model is built purely from configuration.Identity()(:76-:82) is the one call site that passesenableRefreshSessions: true, because refresh sessions are Identity-only data.Conference()(:84-:85) andEngagement()(:87-:88) callBuildwith only a data-source name and configuration assembly, taking every flag default.Notification()(:93-:98) is the one caller that turns audit trail and scheduler off, matching the thin module's factory.Build(:100-:128) is the shared mechanism: it setsoptions.DataSourceNameand a singleDataSourcesentry (:113-:118), soDataSourceResolverseedsDefaultfrom that one named source and the model gets the host-scopedScheduledJobs/RefreshSessionstables the real factories rely on; it then applies the three framework-table flags (:120-:122) and the configuration assembly (:124) before returningcontext.Model(:127). - Why it's built this way - the delete-behavior gate (ADR-119, restrict-delete-by-default) needs the finalized model, foreign keys and all, for every service database, and building it offline (rather than against a real SQL Server) keeps the check in the fast unit/architecture tier.
- Where it's used - DeleteBehaviorConventionTests, the only consumer in the defining file.
- Caveats / not-in-source - none outside the defining file; MMCA.Common's own
DeleteBehaviorConventionTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs) builds its own model the same way but is a separate type in a separate assembly, not a caller of this one.
AbstractAnonymousFixtureControllerBase, AnonymousFixtureController, TypeLevelAnonymousFixtureController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api· (see per-type table) · Level 0 · class
Three throwaway MVC controllers nested inside AnonymousEndpointTestsBaseTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymousEndpointTestsBaseTests.cs). Between them they cover every placement of [AllowAnonymous] the scan has to recognize: on an action of a concrete controller, on an action of an abstract base, and on the controller type itself. They are the input side of the anonymous-endpoint allow-list gate; the subclasses that consume them supply the expectations.
- Depends on -
Microsoft.AspNetCore.Mvc.ControllerBaseandMicrosoft.AspNetCore.Authorization.AllowAnonymousAttribute(AnonymousEndpointTestsBaseTests.cs:1-:2). No state, no behavior: every action is=> Ok(). - Concept introduced - an allow-list is only as good as the identifier shapes it can be written in. AnonymousEndpointTestsBase turns each discovered
[AllowAnonymous]into a string, and there are exactly two shapes: a type-level attribute becomes the type'sFullName, and a method-level attribute becomes{declaring type FullName}.{method name}(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:255-:152). A repo's allow-list is hand-written against those strings, so if the emitter and the hand-written convention ever disagree the gate silently reports phantom offenders or, worse, accepts a stale entry. These three fixtures exist so both shapes are produced by a real scan and matched by a real allow-list in the framework's own CI. Two scoping decisions in the base are also on display here:ControllerBase,RouteAttributeandAllowAnonymousAttributeare all matched by full name through reflection (:32-:34) so the rule package carries no ASP.NET reference, and abstract controllers are deliberately included in the scan (IsControllerwalksBaseType,:101-:112) because a framework base action is where the attribute is declared. [Rubric §11 - Security] assesses whether the authorization posture of the HTTP surface is deliberate; an ungated endpoint that nobody reviewed is the single cheapest way to lose an app. [Rubric §26 - Front-End Security] extends the same scan to routable Blazor components (IsRoutableComponent,:117-:118). [Rubric §14 - Testability] covers the fixtures themselves. - Walkthrough - the scan enumerates
LoadableTypesof each target assembly, keeps controllers and routable components, and projects each survivor throughAnonymousEndpointsOf, distinct and ordinal-ordered (AnonymousEndpointTestsBase.cs:183-:131). Methods are read withBindingFlags.DeclaredOnlyandinherit: false(:142,:146), which is what decides where an inherited action gets reported (see InheritingFixtureController).
| Type | File:Line | The identifier shape it produces |
|---|---|---|
AnonymousFixtureController |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:137 |
The ordinary case: a sealed ControllerBase whose PeekAsync carries [HttpGet] and [AllowAnonymous] (:78-:80). Emits the method-level identifier ...AnonymousFixtureController.PeekAsync, and is the name the drifted subclass's failure message must contain (:22). |
AbstractAnonymousFixtureControllerBase |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:147 |
The declaration site: an abstract controller whose virtual InheritedAnonymousAsync carries [HttpGet("inherited")] and [AllowAnonymous] (:88-:90). Emits one identifier at the base, mirroring how the framework's own AuthControllerBase actions are declared once for every consumer that derives from them. |
TypeLevelAnonymousFixtureController |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:161 |
The other identifier shape: [AllowAnonymous] on the type (:97) with a body-less declaration and no actions at all. Emits the bare FullName, with no method suffix. |
- Why they're built this way - a gate whose fixtures only ever exercise one attribute placement would pass while being blind to the other, and the failure would land in a consumer repo rather than here. Nesting the fixtures inside the test class keeps them out of the assembly's public surface and, critically, out of the framework's real AnonymousEndpointTests scan, which targets the API and UI assemblies rather than this one. See ADR-015.
- Where they're used - scanned by the four nested subclasses DriftedTests, StaleAllowListTests, EmptyScanTests and ConformantTests, and named in the assertions of AnonymousEndpointTestsBaseTests.
AbstractFitnessControllerBase, IdempotentFitnessController, NonIdempotentFitnessController, UndeclaredFitnessController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs· (see per-type table) · Level 0 · class
Four throwaway MVC controllers nested inside IdempotencyFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/IdempotencyFitnessTests.cs). Each is a ControllerBase subclass with exactly one [HttpPost] action, and the only thing that varies between them is which idempotency attribute the action carries. Together they form the truth table for the PostActionsDeclareIdempotencyIntent fitness function: one offender, one covered directly, one covered by inheritance, one opted out with a reason, and one abstract declaration site that must be skipped entirely.
- Depends on -
Microsoft.AspNetCore.Mvc.ControllerBase(IdempotencyFitnessTests.cs:1), plus IdempotentAttribute and NonIdempotentAttribute fromMMCA.Common.API.Idempotency(:2). Nothing else: these types have no state and no behavior worth exercising. - Concept introduced - declared idempotency intent, and the truth table that proves a gate reads it. A POST that is retried by a client, a proxy, or a mobile app on a flaky connection either replays its original response or writes twice. IdempotentAttribute opts an action into the replay path served by IdempotencyFilter; NonIdempotentAttribute records in code why replaying would be wrong (issuing a token, revoking a credential, exchanging a single-use code). What the framework will not accept is silence, so
ArchitectureRules.PostActionsDeclareIdempotencyIntent(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44) fails the build on any POST carrying neither attribute. These four fixtures exist because a gate like that has four distinct behaviors to prove, not one. [Rubric §9 - API & Contract Design] assesses whether the HTTP surface states its own semantics; a declared retry posture is part of the contract, not an implementation detail. [Rubric §29 - Resilience & Business Continuity] is the reason the rule exists at all: retries are the default failure response of every modern client, so a POST with undeclared replay semantics is a duplicate-write incident waiting for a network blip. [Rubric §14 - Testability] covers the fixtures themselves, which turn each branch of the rule into an observable case. - Walkthrough - the rule reads attributes by simple type name (
"IdempotentAttribute","NonIdempotentAttribute","HttpPostAttribute",ArchitectureRules.Idempotency.cs:6,:9,:12) so the rule package keeps no ASP.NET reference, and it reads them withinherit: true(:80-:81), which is what makes the inherited case work. Two filters decide what is even looked at:.SelectMany(a => a.ConcreteClasses)(:49), whose helper keeps only non-abstract classes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:36-:37), and.Where(IsController)(:51).
| Type | File:Line | The case it models |
|---|---|---|
IdempotentFitnessController |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:61 |
The primary covered shape: CreateThingAsync carries [HttpPost] and [Idempotent] on the action itself (:65-:67). The rule must not report it. |
AbstractFitnessControllerBase |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:71 |
The declaration site: an abstract controller whose virtual CreateInheritedAsync carries [HttpPost("inherited")] and [Idempotent] (:75-:77). Because it is abstract it never reaches ConcreteClasses, so it is skipped rather than judged; its concrete subclasses are what actually route. |
NonIdempotentFitnessController |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:84 |
The justified opt-out: IssueTokenAsync carries [NonIdempotent("Issues a token; a replayed response would hand back credentials minted for an earlier call.")] (:88-:90). A declaration, not an omission, so the rule stays quiet. |
UndeclaredFitnessController |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:94 |
The offender: a bare [HttpPost] CreateThingAsync with neither attribute (:98-:99). This is the one name the rule's exception message must contain. |
- Why they're built this way - a fitness function that only ever sees compliant code proves nothing, and one that flags everything is worse than none. Four controllers whose only difference is the attribute set turn the rule into something with a pass and a fail on either side of each branch (ADR-015). Keeping them nested inside the test class keeps them out of the assembly's public surface and out of any other rule's reach.
- Where they're used - reflected over by IdempotencyFitnessTests through IdempotencyTestMap, which registers this test assembly as the map's single Api layer.
- Caveats / not-in-source - these fixtures deliberately derive from raw
ControllerBaserather than ApiControllerBase, which the framework's separate controller-inheritance rule would flag. They escape it only because that rule runs over CommonArchitectureMap, which does not include this test assembly.
ArchiveTicketCommand, CreateTicketCommand, PurgeTicketsCommand, RebuildTicketIndexCommand, ReopenTicketRequest, UpdateTicketRequest
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures· (see per-type table) · Level 0 · record
Six internal sealed record payload types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs), each a positional record with at most one property. They are the input side of the command-validation coverage truth table: four commands that a handler will pick up, and two request records that a bridged command embeds.
- Depends on - nothing beyond the BCL, except that the file as a whole references ICommandHandler<in TCommand, TResult> and ICommandWithRequest<out TRequest> from
MMCA.Common.Application.UseCases(CommandValidatorFixtures.cs:2) and FluentValidation (:1). These six records implement neither: they are pure payload. - Concept introduced - what makes a command "data-carrying", and why that decides whether it is judged at all. The validation-coverage rule does not ask "does every command have a validator". It asks the narrower question "does every command that could carry bad input have something checking it", and the test for that is mechanical: at least one public instance property with a public setter,
initincluded (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:135-:138). A positional record is exactly that shape, which is why the usual command is inspected and why a payload-free marker command is skipped as having nothing to validate (rule remarks,:19-:21). The rule then keeps only commands whose type appears as theTCommandof some concreteICommandHandlerin scope, and only closed ones: an open generic handler base contributes a type parameter, not a command a repo ships (:96-:102). [Rubric §6 - CQRS & Event-Driven] assesses whether the command surface is a governed contract rather than an ad-hoc bag of DTOs. [Rubric §24 - Forms/Validation/UX Safety] is the property being defended at its far end: the Validating decorator runs before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler and the pipeline stage exists with nothing to run (rule doc,:9-:13). [Rubric §14 - Testability] covers the fixtures. - Walkthrough - the file's header doc (
:7-:12) enumerates the shapes deliberately: a command covered by its own validator, one covered only through the request bridge, one covered by neither, one carrying no payload at all, and a bridge command whose request has no validator.
| Type | File:Line | The case it models |
|---|---|---|
CreateTicketCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:13 |
record CreateTicketCommand(string Title). Direct coverage: CreateTicketCommandValidator validates it explicitly, so it must be absent from the report. |
UpdateTicketRequest |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:28 |
record UpdateTicketRequest(string Title), the request a bridged command embeds. Never a command itself (no handler takes it), so the rule never judges it; it exists to be the validated half of the bridge. |
ReopenTicketRequest |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:45 |
record ReopenTicketRequest(string Reason), deliberately with no validator (doc, :44). It is what makes the half-bridge case possible. |
ArchiveTicketCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:60 |
record ArchiveTicketCommand(Guid TicketId). Carries data, has a handler, has no validation of any kind (doc, :59). The primary offender, and the name the failure message must contain. |
PurgeTicketsCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:69 |
record PurgeTicketsCommand(DateTime OlderThan). A second uncovered command, kept separate so one fact can allowlist it and prove an exemption silences exactly the command it names while the other offender still fails (doc, :68). |
RebuildTicketIndexCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:78 |
record RebuildTicketIndexCommand(), a parameterless marker with no settable property (doc, :77). Skipped by HasSettablePublicProperty, so it is neither covered nor reported, and it is the one command excluded from the inventory count. |
- Why they're built this way - two uncovered commands rather than one is the load-bearing choice: an allowlist test that had only a single offender could not distinguish "the exemption worked" from "the rule stopped firing". Keeping every record to one property means a change in the rule's classifier is diagnosed by which fixture moved rather than by reading a wall of assertion output. See ADR-015.
- Where they're used - handled by the six fixture handlers (ArchiveTicketHandler and family), scanned through FixtureModuleMap, and named by
nameofin the assertions of CommandValidatorCoverageFitnessTests.
ArgumentGuardFixture, IndirectThrowFixture, InvalidOperationThrowingFixture, NonThrowingFixture, RethrowingFixture, TicketDomainException
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainThrowFixtures· (see per-type table) · Level 0 · class
Six types in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs): five static classes holding compiled throw sites and one custom exception for them to throw. They are the truth table for the domain-throw rule, which reads IL, so the only honest way to test it is to compile the throws it must and must not flag into this assembly and point a map at it (file doc, :3-:8).
- Depends on - the BCL only:
ArgumentException,ArgumentNullException,ArgumentOutOfRangeException,InvalidOperationException,Exception,Action,ICollection<string>. - Concept introduced - the Result pattern's build-time boundary, and the three exceptions it still allows. ADR-013 says the domain reports failure by returning Result, never by throwing: a thrown business failure skips
Result.Combineinvariant composition, turns a 4xx outcome into a 500, and pays an exception unwind on a path that is not exceptional (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:26-:29). The rule is not "never throw", though. Argument guards report a caller bug rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for aResultto carry; exactly three exception types are permitted (:16-:21). Everything else in a Domain assembly is a violation. The mechanism is deliberately narrow: for eachthrowopcode the rule looks at the immediately preceding instruction, skipping thenops a Debug build interleaves, and reads the exception type off anewobj(:143-:154). If the preceding instruction is anything else, the thrown value came from somewhere the instruction stream cannot name, and the site is reported as UNVERIFIABLE rather than guessed at (:127-:131). [Rubric §4 - DDD] assesses whether the domain expresses outcomes in its own vocabulary; an exception is control flow borrowed from the host. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] cover the rest. - Walkthrough - a bare
throw;inside acatchcompiles to the distinctrethrowopcode, notthrow, so it never even enters the scan (fixture doc,:72-:75). The rule also skips compiler-synthesized bodies: the skeleton members of a C#extension(T)block, and the explicit interface implementations on compiler-generated types such as the read-only list wrapper emitted for a collection expression (:156-:177).
| Type | File:Line | What it models |
|---|---|---|
ArgumentGuardFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:9 |
All three permitted guards in one type: ArgumentNullException via a null-coalescing throw (:11-:12), ArgumentOutOfRangeException from a range check (:14-:20), and ArgumentException after an ArgumentNullException.ThrowIfNull (:22-:30). The whole type name must be absent from the report. |
InvalidOperationThrowingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:37 |
The canonical offender: a business outcome signalled by throwing, throw new InvalidOperationException("The ticket is already closed") (:43). Its doc names the fix, a Result.Failure (:33-:36). |
TicketDomainException |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:49 |
The only public type in the file: a three-constructor Exception subclass (:51-:63). It carries no throw of its own; it exists so the rule can be shown to judge by the constructed type rather than by a name allowlist, since a custom exception is the same defect wearing a domain name (doc, :48). |
RethrowingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:76 |
The negative control for opcode discrimination: a try/catch that logs and then rethrows bare (:83-:91). Preserving a caught exception must stay free, and the rethrow opcode is what makes that possible without an allowlist entry. |
IndirectThrowFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:99 |
The blind spot, stated honestly: Fail(Exception prepared) => throw prepared (:101). The value was constructed elsewhere, so the preceding instruction is not a newobj and the rule reports UNVERIFIABLE instead of passing or failing it. |
NonThrowingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:105 |
The shape the rule exists to protect: CanClose(bool alreadyClosed) => !alreadyClosed (:107). Returns instead of throwing. |
- Why they're built this way - the rule has five outcomes (allowed guard, business exception, custom exception, rethrow, unverifiable) plus a clean case, and each needs a subject that trips exactly one of them. Every fixture except
TicketDomainExceptionisinternal static, so nothing here widens the assembly's public surface, and putting them in their own namespace lets one allowlist entry silence the whole set for the escape-hatch facts. See ADR-015. - Where they're used - scanned by DomainThrowFitnessTests through its FixtureAssemblyMap, which registers this test assembly as the map's single Domain layer.
CompliantFixtureService, MissingTokenFixtureService, MisplacedTokenFixtureService, MisnamedTokenFixtureService, ExemptableFixtureService, ExternalContractFixtureService
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CancellationFixtures· (see per-type table) · Level 0 · class
Six tiny services in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs), each a hand-shaped input for one branch of the AsyncMethodsDeclareTrailingCancellationToken fitness function. None of them does any work; the method signatures are the fixture.
- Depends on - the BCL only:
Task/ValueTask,CancellationToken,IAsyncEnumerator<T>, and one[SuppressMessage]fromSystem.Diagnostics.CodeAnalysis(CancellationTokenFixtures.cs:1). - Concept introduced - the trailing-token convention, and why it is enforced mechanically. Work that cannot be cancelled keeps running against the database after its caller is gone. The framework's rule is not "accept a token somewhere" but the far stricter "every public awaitable method on an Application or Infrastructure type ends with a parameter of type
CancellationTokennamed exactlycancellationToken" (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35). The uniformity is the point: only a positionally and nominally fixed parameter lets the decorator pipeline and the repositories forward a linked token mechanically, without a per-method decision. The rule scans only the Application and Infrastructure layers (:45-:47), only publicly visible non-delegate types (:92-:94), and only public, non-special-name, non-Disposemethods whose return type isTask,Task<T>,ValueTask, orValueTask<T>(:97-:103).TokenProblem(:115) then classifies each survivor into one of three messages:"no CancellationToken parameter"(:134),"the CancellationToken must be the LAST parameter"(:131), or"the trailing CancellationToken must be named 'cancellationToken'"(:130). [Rubric §12 - Performance & Scalability] assesses whether the system sheds abandoned work instead of paying for it; an uncancellable query holds a connection for the full duration of a request nobody is waiting for. [Rubric §6 - CQRS & Event-Driven Design] applies because cancellation is plumbing that has to be uniform to be automatable, and [Rubric §14 - Testability] because these six fixtures are what make the classifier provable rather than assumed. - Walkthrough - two escape hatches also need fixtures, and they work differently. The explicit one is the
exemptMethodsparameter (ArchitectureRules.CancellationTokens.cs:37), a caller-supplied list matched as"TypeName.MethodName"(:78); it exists for shipped public APIs where adding a parameter would be a breaking change. The automatic one isExternallyFixedMethods(:141), which drops any method that overrides a base declared outside the map's assemblies or implicitly implements an interface declared outside them: a signature this repo does not own is not this repo's to fix.
| Type | File:Line | What it models |
|---|---|---|
CompliantFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:6 |
The clean case, and two out-of-scope shapes in the same type: GetAsync(string, CancellationToken) and DoAsync(CancellationToken) both comply (:9-:17), Count(string) is not awaitable (:20), and HiddenAsync() is internal (:23), so neither is scanned. The whole type's name must be absent from the report. |
MissingTokenFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:27 |
No token at all, twice: RunAsync(string) (:30) and PingAsync() (:33). The parameterless one is deliberate: it pins the ruling that a method with no parameters is not excused, because the token would simply be its only one. |
MisplacedTokenFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:37 |
The token leads instead of trailing: RunAsync(CancellationToken, string) (:44). Writing it required suppressing CA1068, and the justification says exactly why (:40-:43), which is the honest way to keep an analyzer-hostile fixture in a warnings-as-errors repo. |
MisnamedTokenFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:49 |
The token trails but is named token (:52). Proves the rule checks the name, not just the position and type. |
ExemptableFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:57 |
A plain offender, LegacyAsync(string) (:60), used twice by the test: once with no exemptions (reported) and once listed in exemptMethods (silent). It is what makes the explicit escape hatch provable. |
ExternalContractFixtureService |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs:67 |
An IAsyncEnumerator<int> implementation whose MoveNextAsync() (:73) is awaitable, public, and token-less, yet is declared by the BCL. Proves the automatic exemption: an implicit implementation of an interface outside the map is never flagged. |
- Why they're built this way - the six shapes are the rule's entire decision surface. Putting them in their own
CancellationFixturesnamespace keeps them out of the way of the other fitness maps in this assembly, and keeping every one of them trivial means a future change to the rule's classifier is diagnosed by which fixture moved, not by a wall of assertion output. See ADR-015. - Where they're used - driven entirely by CancellationTokenFitnessTests through CancellationTestMap.
AsyncClockReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:24· Level 0 · class
- What it is - a fixture whose clock read happens inside an
asyncbody,internal sealed class AsyncClockReadingFixturewithpublic async Task<DateTimeOffset> StampAsync()that awaitsTask.Yield()before returningDateTimeOffset.UtcNow(ClockReadFixtures.cs:24-:32). - Depends on - nothing beyond the BCL:
Task,Task.Yield(), andDateTimeOffset.UtcNow(:29-:30). - Concept introduced - why the clock-read rule has to follow a compiler-generated state machine, not just the method body as written. An
asyncmethod with anawaitis rewritten by the compiler into a state machine type, so the actualDateTimeOffset.UtcNowread lives on a nestedMoveNextrather than onStampAsyncitself; a rule that only enumerates a type's own IL would miss it.ClockReadTestsBasemust resolve that nested read back to the member that wrote the source, the same requirement a lambda body creates for LambdaClockReadingFixture, soClockReadTestsproves both cases side by side. - Walkthrough - one method,
StampAsync()(:27-:31):await Task.Yield()(:29) forces the compiler to build the state machine, thenreturn DateTimeOffset.UtcNow(:30) is the flagged read. - Where it's used - asserted by
ClockReadTests.ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember, which requires the report contain"AsyncClockReadingFixture.StampAsync reads DateTimeOffset.UtcNow"(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:46).
AsyncLambdaClockReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:54· Level 0 · class
- What it is - a fixture whose clock read sits inside an
asynclambda,internal sealed class AsyncLambdaClockReadingFixturewithpublic Func<Task<DateTime>> Clock()returningasync () => { await Task.Yield(); return DateTime.UtcNow; }(ClockReadFixtures.cs:54-:61). - Depends on - nothing beyond the BCL:
Func<>,Task,Task.Yield()andDateTime.UtcNow(:56-:60). - Concept introduced - why the clock-read rule has to handle a lambda and an async body at the same time. AsyncClockReadingFixture covers an
asyncmethod and LambdaClockReadingFixture covers a plain lambda; this one combines them, so theDateTime.UtcNowread lives on a compiler-generated state machine nested inside a closure class, two levels away from the source memberClock. The rule must resolve it back toClockfor the report, and an allow-list entry forClockmust also cover it. - Walkthrough -
Clock()(:56-:60) returns the lambda without running it; inside the lambdaawait Task.Yield()(:58) forces the state machine andreturn DateTime.UtcNow(:59) is the flagged read. - Where it's used - asserted by
ClockReadTests.ReadsInsideAnAsyncLambda_AreAttributedToTheSourceMemberAndExemptedWithIt(rolled up under per-project test rollup): the report must contain"AsyncLambdaClockReadingFixture.Clock reads DateTime.UtcNow", and an allow-list entry for...AsyncLambdaClockReadingFixture.Clockmust remove it (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:54-:61).
Declaration
MMCA.Common.Architecture.Tests · (global namespace) ·
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/JwtAudienceFixtures/Clean/Program.cs:14· Level 0 · class
- What it is - a near-miss in the conforming JWT-audience fixture,
public static class Declarationwhose only member ispublic static IServiceCollection AddForwardedJwtBearer(IServiceCollection services) => services(Program.cs:14-:17). It is a method declaration with the same name as the audited call, not a call. - Depends on -
IServiceCollectiononly (:16). - Concept introduced - why a text-scanning fitness rule needs fixtures that look like the target but are not. The file is uncompiled text (its header says it is excluded from compilation in the csproj,
:1) read by the forwarded-JWT-audience gate. Next to the real call (:8-:12, with the audience fromJwtAudience.RequireConfigured), it carries a comment mention (:7), a doc-comment mention (:4) and this declaration, so the rule is proved not to count them as calls. - Walkthrough - nothing executes; the class exists only so the scanner meets a line containing
AddForwardedJwtBearer(that is a declaration. - Where it's used -
ForwardedJwtAudienceFitnessTestspointsArchitectureRules.ForwardedJwtBearerAudienceIsFailClosedat theCleanfolder and expects it to pass withminimumCalls: 1and to fail the over-count withminimumCalls: 2, which shows exactly one call was counted (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs:24,:31-:32).
EngineHit
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataSourceBranchingFitnessTests.cs:149· Level 0 · record
- What it is - the one-line scan result of the data-source branching gate,
private sealed record EngineHit(string File, int Line, string Text)nested insideDataSourceBranchingFitnessTests(DataSourceBranchingFitnessTests.cs:149). Its doc names the three fields: the file path relative toSource/, the 1-based line number, and the line text (:148). - Depends on - nothing beyond the BCL; three positional properties, no members of its own.
- Concept introduced - why a text-scan fitness gate keeps each hit as a value, not a formatted string. Engine-specific behavior is meant to live behind IDataSourceEngine (ADR-130), and nothing in the compiler stops a new
DataSource.Sqlitecomparison oris SqliteDbContextcheck from growing back outside the strategies, so the gate scansSource/**/*.csas text against a file-granular allow-list (class doc,:7-:16). Keeping the hit structured lets two opposite assertions share one scan:ConcreteEngineBranching_StaysInsideTheEngineStrategiesfilters hits whoseFileis not allow-listed (:73-:77), andAllowList_HasNoStaleEntriesprojects the same hits to a set ofFilevalues and fails on any allow-listed file that no longer produces one (:85-:89), so the allow-list shrinks with the code. [Rubric §14 - Testability] covers the gate. - Walkthrough - built only in
ScanSource()(:112-:140): for every.csfile under the repo'sSourceroot, excludingbinandobj(:114-:120), each line matching theEngineReferenceregex (:142-:146) becomesnew EngineHit(relative, i + 1, lines[i].Trim())with the path normalized to forward slashes (:128,:134). Before scanning, the method asserts more than 500 files were found so a wrong scan root cannot pass vacuously (:122-:123). The violating hits are rendered asSource/{File}:{Line}: {Text}and handed to ArchitectureAssert (:77-:79). - Where it's used - private to
DataSourceBranchingFitnessTests(rolled up under per-project test rollup); not referenced outside the defining file.
FatFixtureController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:17· Level 0 · class
- What it is - a top-level
[ApiController]fixture with three injected string collaborators,internal sealed class FatFixtureController(string first, string second, string third) : ControllerBase(ConstructorDependencyFixtures.cs:17), built to sit one dependency over a tight ceiling. - Depends on -
Microsoft.AspNetCore.Mvc.ApiControllerAttribute/ControllerBase(:1,:16-:17) only; each injectedstringis read back throughCollaborators(:19) so none is an unread primary-constructor parameter (CS9113). - Concept introduced - why the constructor-dependency ceiling needed a controller and a handler population, not just the Application
*Serviceclasses it already covered.ConstructorDependencyCountTestsBasegainedControllers_DoNotExceedConstructorDependencyCeilingandHandlers_DoNotExceedConstructorDependencyCeilingagainstMap.Api()andMap.ModuleApplication(), and this fixture is the offender each assertion must name. It is deliberately top-level, never nested, because the gate skips nested types (file doc,:10-:11). - Walkthrough -
ConstructorDependencyCountTestsBaseTestswires it through a privateFixtureMapwhose controller ceiling is fixed to 2 (ConstructorDependencyCountTestsBaseTests.cs:151), soFatFixtureController's three constructor dependencies exceed it and the failure message must containFatFixtureController (3 ctor dependencies)(:23); the same map with the ceiling raised to 3 (:116) must not flag it. - Where it's used - referenced by
ConstructorDependencyCountTestsBaseTests(:23,:81-:82), and shares its file with a thinner one-dependency sibling controller, a payload-free marker command and handler, and the query/handler pair below, each proving a different branch of the same ceiling.
FixturePiiSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.PiiInventoryFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PiiInventoryFixtures/PiiInventoryFixtures.cs:10· Level 0 · class
- What it is - the data subject the personal-data inventory fixtures describe:
internal sealed class FixturePiiSubjectwith two[Pii]string members (Email,Phone) and one unmarkedint Id(PiiInventoryFixtures.cs:10-:22). Its doc names thepii-inventory-*.mdfiles beside it as the inventories it is checked against (:5-:9). - Depends on -
MMCA.Common.Domain.Attributesfor the[Pii]marker (:1) and nothing else. - Concept introduced - a personal-data inventory is a two-way contract between code and a document. The inventory rule demands a row, with a lawful-basis cell, for every
[Pii]member and a live[Pii]member behind every row. The unmarkedIdproves the rule does not demand rows for non-personal members (:12). [Rubric §34 - Architecture Governance & Documentation] covers keeping such policy documents and code from drifting; [Rubric §14 - Testability] is why the subject is compiled rather than described. - Walkthrough -
PiiInventoryTestsBaseTestspoints the rule at this assembly (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs:16) and asserts four failures:FixturePiiSubject.Phone has no rowfor a missing row (:28), an emptyLawful basiscell reported forPhonewhile the completeEmailrow is not (:38-:39), and a row forFixturePiiSubject.Faxthat names no live[Pii]member (:48). - Where it's used - only by
PiiInventoryTestsBaseTests(:11,:16,:28-:48).
FixtureShape
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:9· Level 0 · class
- What it is - the abstract base for the substitutability fixtures:
internal abstract class FixtureShapewith an abstractCorners()and a virtualDescribe()returning"shape"(SubstitutabilityFixtures.cs:9-:14). - Depends on - nothing.
- Concept introduced - Liskov substitutability read from IL. The rule behind
ArchitectureRules.SubstitutableMembersDoNotRefuseTheContract(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:25) flags an override that throws instead of honouring what its base promised. A base that offers both an abstract and a virtual member lets the fixtures refuse each one. [Rubric §34 - Architecture Governance & Documentation] covers the property; the fixture file's doc says an IL-reading rule can only be tested honestly by compiling the cases into the assembly (:3-:8). - Walkthrough - it has no behavior of its own.
FixtureRefusingShapeoverridesDescribewith aNotSupportedException(:21) and must be flagged;FixtureExcludedShaperefusesCorners(:52) and is silenced only when the test lists it by name (SubstitutabilityRuleTests.cs:18,:62-:69). - Where it's used - derived by
FixtureRefusingShape(:17) andFixtureExcludedShape(:50); not referenced outside the defining file.
FixtureStandaloneThrower
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:40· Level 0 · class
- What it is - the innocent control in the substitutability fixtures:
internal static class FixtureStandaloneThrowerwhoseLoad(bool supported)throwsNotSupportedExceptionwhensupportedis false (SubstitutabilityFixtures.cs:40-:44). - Depends on - nothing beyond the BCL.
- Concept introduced - the rule's negative space. A throw is only a broken promise when something promised the operation. This method overrides nothing and implements nothing, so no caller was promised it through a base type, and the rule must leave it alone (doc,
:36-:39). Without this control, a rule that flagged every throwing method would pass the other fixtures. [Rubric §14 - Testability]. - Walkthrough -
NonOverrideMethodThatThrows_IsNotFlaggedruns the rule over the fixture assembly, expects it to fail for the other fixtures, and asserts the report does not containFixtureStandaloneThrower(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:45-:53). - Where it's used - referenced once, by
SubstitutabilityRuleTests(:51).
GetFixtureFarQuery
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SliceFixtures.Stranded·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/GetFixtureFarQuery.cs:7· Level 0 · record
- What it is - a payload-only query record,
internal sealed record GetFixtureFarQuery(int Id)(GetFixtureFarQuery.cs:7), declared in theStrandednamespace one level below the handler that serves it. Its doc names that handler,FixtureTwoContractHandlerBase, and says it is handled beside a second stranded contract (:3-:6). - Depends on - nothing beyond the BCL. It is the first
TQuerythatFixtureTwoContractHandlerBasecloses IQueryHandler<TQuery, TResult> over (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:13). - Concept introduced - a handler is checked against every contract it serves, not only the first one reflection lists. Registration wires a handler class for every handler interface it implements, so a class implementing two query handlers serves two slices, and the slice-cohesion rule has to judge each contract on its own (
FixtureTwoContractHandlerBase.cs:7-:11). This record is one of the two contracts that make that observable: both live one namespace away from the handler, so a rule that stopped at the first interface would report one and miss the other. The co-located-versus-stranded idea itself is introduced under GetFixturePreferencesQuery. [Rubric §5, Vertical Slice] is the property; [Rubric §14, Testability] covers the fixture. - Walkthrough - one property,
int Id(:7). No behavior;FixtureTwoContractHandlerBase.HandleAsync(GetFixtureFarQuery, CancellationToken)returnsResult.Success()(FixtureTwoContractHandlerBase.cs:15-:16) and is never called, only reflected over. - Where it's used - handled by
FixtureTwoContractHandlerBase(FixtureTwoContractHandlerBase.cs:13,:15), and named in the failure message thatAHandlerServingTwoStrandedContracts_IsFlaggedForBothasserts in SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:62), which runsArchitectureRules.HandlersAreCoLocatedWithTheirContracts(:59).
GetFixturePreferencesQuery
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SliceFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:13· Level 0 · record
- What it is - a payload-only query record,
internal sealed record GetFixturePreferencesQuery(int UserId)(SliceFixtures.cs:13), that exists purely as the contract type three abstract handler shapes close over in the slice-cohesion fitness truth table. - Depends on - nothing beyond the BCL. It implements no interface itself; it is the
TQuerythat GetFixturePreferencesHandlerBase and StrandedFixtureHandlerBase close IQueryHandler<TQuery, TResult> over. - Concept introduced - why the slice-cohesion rule needs an abstract handler in its truth table, not just a concrete one.
SliceCohesionTestsBase's rule used to read only concrete classes, so an abstract handler whose contract lived one namespace away from its declaration was invisible to it. The file's own header doc says these types are exactly what the rule started seeing once it was widened (SliceFixtures.cs:7-:11). Every handler over this query is deliberately abstract, and the query is deliberately a plain record rather than a command, so the file's fixtures never register as commands and skew the separate command-validator coverage rule, which also points a map at this assembly (:9-:11). [Rubric §5, Vertical Slice] is the property: a handler and the contract it serves belong in the same namespace so a slice reads as one cohesive unit; [Rubric §14, Testability] covers the fixture itself. - Walkthrough - one property,
int UserId(:13). No behavior; its only role is to be the closed generic argument three handler shapes are proven against. - Where it's used - closed over by GetFixturePreferencesHandlerBase (
SliceFixtures.cs:19) as the co-located case the rule must pass, by StrandedFixtureHandlerBase (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11) as the one-namespace-away case the rule must catch, and registered as this assembly'sLayer.Applicationin SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:76).
GetFixtureProjectionQuery
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:45· Level 0 · record
- What it is - a payload-only query record,
internal sealed record GetFixtureProjectionQuery(int ProjectionId)(:45), declared immediately beside the handler that serves it. - Depends on - nothing beyond the BCL; it is the
TQuerythatGetFixtureProjectionHandlerclosesIQueryHandler<TQuery, TResult>over (:48-:49). - Concept introduced - slice cohesion as a byproduct of a different fixture's design. The file groups the query beside its handler deliberately (doc,
:9), which keeps the constructor-dependency ceiling fixtures out of the separate command-validator coverage rule: as a plain record with no settable property, it is not scanned as a command and does not skew the count of data-carrying commands the file's marker command already fixes (file doc,:12-:14). - Walkthrough - one property,
int ProjectionId(:45). No behavior; its only role is to be the closed generic argumentGetFixtureProjectionHandler's three injected collaborators are proven against. - Where it's used - closed over by
GetFixtureProjectionHandlerin the same file (:48-:49); not referenced outside the defining file.
GetFixtureSecondFarQuery
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SliceFixtures.Stranded·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/GetFixtureSecondFarQuery.cs:4· Level 0 · record
- What it is - the second stranded contract,
internal sealed record GetFixtureSecondFarQuery(int Id)(GetFixtureSecondFarQuery.cs:4), which its doc describes as the second contractFixtureTwoContractHandlerBasehandles (:3). - Depends on - nothing beyond the BCL. It is the second
TQuerythatFixtureTwoContractHandlerBasecloses IQueryHandler<TQuery, TResult> over (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:13). - Concept introduced - cross-reference: the "every contract, not just the first" case is introduced under GetFixtureFarQuery. This record is the half that a first-interface-only rule would miss, so its presence in the failure message is the actual proof.
- Walkthrough - one property,
int Id(:4). No behavior; its handler overload returnsResult.Success()(FixtureTwoContractHandlerBase.cs:18-:19) and is only reflected over. - Where it's used - handled by
FixtureTwoContractHandlerBase(FixtureTwoContractHandlerBase.cs:13,:18), and asserted in the failure message ofAHandlerServingTwoStrandedContracts_IsFlaggedForBothin SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:63).
IBadgeGranter
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:64· Level 0 · interface
- What it is - a one-method fixture abstraction with a single implementation, existing only so that one of the domain-event handler fixtures reaches its save through an interface call rather than a direct one.
- Depends on - nothing.
internal interface IBadgeGranterwithTask GrantAsync(CancellationToken cancellationToken)(DomainEventSaveFixtures.cs:64-:67). - Concept introduced - why an IL walk has to resolve interface dispatch, and why a fixture must force it to. Domain event handlers run inside the save that raised the event: dispatch happens after
SaveChangesAsync, or after commit inside a transactional command. A handler that saves again therefore opens a second write in the middle of the first one, re-entering the change tracker, possibly raising a fresh event cascade, and persisting work the outer transaction may still roll back (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:23-:29). Catching that means following what a handler calls, and in IL a call through an abstraction records only the interface method:callvirt IBadgeGranter::GrantAsync, with no trace of the implementation that will actually run. A walker that stopped there would report nothing. The rule therefore expands each callee to the implementations in scope through CallGraphIndex, and this interface is the fixture that makes that expansion observable. The doc says it in one line: the collaborator is an abstraction, so IL records only the interface call (:63). [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §14 - Testability] is why the fixture exists. Full mechanism at DomainEventHandlerSaveFitnessTests. - Walkthrough - one member,
GrantAsync(CancellationToken)(:66). It takes a token but returns nothing meaningful; the signature exists only to be callable. - Where it's used - implemented by BadgeGranter (
:70) and injected into InterfaceDispatchSavingHandler (:83).
IFixtureExporter
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:25· Level 0 · interface
- What it is - a one-method promise,
internal interface IFixtureExporter { string Export(); }(SubstitutabilityFixtures.cs:25-:28), that the next fixture breaks. - Depends on - nothing.
- Concept introduced - the interface half of the substitutability rule. It checks interface implementations as well as overrides:
FixtureUnfinishedExporter.ExportthrowsNotImplementedException(:33), a placeholder that was never written, and the rule must name it (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:34-:42). Companion of FixtureShape, which covers the override half. [Rubric §34 - Architecture Governance & Documentation]. - Walkthrough - one member,
Export()(:27), callable and nothing more. - Where it's used - implemented by
FixtureUnfinishedExporter(:31); not referenced outside the defining file.
InjectedClockFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:34· Level 0 · class
- What it is - a compiled-in fixture behind
ClockReadTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15) that takes a constructor-injectedTimeProvider timeProviderand callstimeProvider.GetUtcNow()(ClockReadFixtures.cs:34-:37): the sanctioned shape the clock-read rule asks for. - Depends on -
System.TimeProvider(BCL) only. - Concept introduced - fitness rules that read IL instead of source.
ArchitectureRules.DomainAndApplicationDoNotReadTheClock(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:59) loads each Domain/Application assembly with Mono.Cecil and scans every method body, lambdas and async state machines included since the compiler turns both into nested types, for a call to one of four ambient-clock getters:DateTime.UtcNow,DateTime.Now,DateTimeOffset.UtcNow,DateTimeOffset.Now(:20-:26,:38-:41). A clock read buried in a business rule cannot be driven by a test, so every expiry or "is it overdue" branch it guards is either untested or tested by sleeping (:32-:35). This fixture proves the rule stays silent when time is taken as an input rather than read from the ambient clock, the complement to LambdaClockReadingFixture and OffsetNowReadingFixture, which prove the rule fires. [Rubric §34 - Architecture Governance & Documentation] covers exactly this kind of self-verifying fitness function: the rule's own test fixtures are compiled, not asserted against a description of the rule. - Walkthrough - one method,
Stamp(), returningtimeProvider.GetUtcNow()(:36). Never executed; only its IL is read. - Why it's built this way - the file's own class-level comment states the rule reads IL, so its behavior must be pinned against real method bodies of each shape rather than against a reading of the rule (
ClockReadFixtures.cs:3-:4). - Where it's used - exercised by
ClockReadTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs), which drivesClockReadTestsBase.DomainAndApplicationDoNotReadTheClock.
LambdaClockReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:18· Level 0 · class
- What it is - a compiled-in offender fixture whose
Clock()method returns aFunc<DateTime>that readsDateTime.UtcNowfrom inside a lambda body (ClockReadFixtures.cs:18-:21); the compiler moves that lambda into a nested type, so the fixture proves the clock-read rule (see InjectedClockFixture) still attributes the violation back to the declaring member. - Depends on - nothing beyond
System.DateTime(BCL). - Concept introduced - cross-reference: the IL-scanning approach is introduced under InjectedClockFixture. This fixture is the "lambda closure" edge case:
ClockReadsIn(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:99) resolves a compiler-generated nested type back to the source member that declared it, so a violation insideClock()'s lambda is still reported and allowlisted againstClock, not against an anonymous internal type name. - Walkthrough - one method,
Clock(), returning() => DateTime.UtcNow(:20). - Why it's built this way - proves the rule's owner-attribution works for the lambda case, not just a plain method body.
- Where it's used - exercised by
ClockReadTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs).
LeftModelBase
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CycleFixtures.Left·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:13· Level 0 · class
- What it is - one arm of a deliberately-built namespace cycle: an abstract base living in the
Leftfixture namespace, which a type in theRightnamespace derives from. That inheritance edge is what closes the loop back intoLeft. - Depends on - nothing. It is
public abstract class LeftModelBasewith a singleint Id { get; set; }(LeftFixtures.cs:13-:17). - Concept introduced - namespace cycles, and how a structural rule sees them.
ArchitectureRules.NamespacesHaveNoDependencyCycles(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45) groups a layer's types by the namespace segment directly beneath the layer'sRootNamespace, records a directed edgeA -> Bwhenever a type inAstructurally references a type inBin the same assembly, and reports every strongly connected component of that graph (:12-:16). "Structurally" is precise here: base types, implemented interfaces, field and property types, method return and parameter types (public and non-public), and attribute types, with generics and array elements expanded (:25-:27). What it deliberately cannot see is a reference that exists only inside a method body, because the package carries no IL or Roslyn dependency (:30-:36), so a clean report is a statement about the type surface, not a claim of zero coupling. A base class is therefore the cheapest possible cycle edge to build: inheritance is always visible to reflection. [Rubric §34 - Architecture Governance & Documentation] assesses whether the codebase stays reasonable to reason about, and a two-namespace cycle is the classic early warning that two folders can no longer be understood, tested, or moved independently. [Rubric §7 - Microservices Readiness] is the sharper consequence: neither namespace can be extracted without the other. - Walkthrough - one auto-property,
int Id(:16), present only so the type is not empty. Beingabstractis irrelevant to the cycle rule (which walks the type surface, notConcreteClasses); what matters is that RightModel names it as a base. - Why it's built this way - the fixture pair models the smallest honest cycle: one edge through a property type and one through inheritance, so the rule is proven against both edge kinds it claims to detect rather than just the easy one.
- Where it's used - the base of RightModel; the pair drives NamespaceCycleFitnessTests through CycleTestMap.
NotAnonymizableSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AnonymizationFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:58· Level 0 · class
- What it is - the control entity of the anonymization fixtures:
internal sealed class NotAnonymizableSubjectwith oneNicknameproperty and noIAnonymizableimplementation (AnonymizationFixtures.cs:58-:61). - Depends on - nothing. Its siblings implement
IAnonymizable(:16,:37,:46,:52); this one deliberately does not. - Concept introduced - reverse-PII rules only judge types that can erase themselves. A plain entity with no erasure path gives neither rule anything to say (doc,
:57). The file header adds that no fixture carries[Pii], because the personal-data inventory self-test scans the whole assembly and a marked fixture would need a row in every inventory fixture (:6-:9; see FixturePiiSubject). [Rubric §14 - Testability]. - Walkthrough -
AnonymizationRulesTestsexpects the sample-key rule to reportNotAnonymizableSubject: the sample key does not implement(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:86-:89) and the erasure-path report to not name it (:124). - Where it's used - only by
AnonymizationRulesTests(:86,:111,:124,:135).
OffsetNowReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:12· Level 0 · class
- What it is - a compiled-in offender fixture whose
Stamp()method readsDateTimeOffset.Nowdirectly, the local-time variant of the ambient-clock read on the other clock type (ClockReadFixtures.cs:12-:15). - Depends on - nothing beyond
System.DateTimeOffset(BCL). - Concept introduced - cross-reference: the IL-scanning approach is introduced under InjectedClockFixture. This fixture is the plainest offender shape: a direct getter call in an ordinary method body, with no lambda or async indirection.
- Walkthrough - one method,
Stamp(), returningDateTimeOffset.Now(:14). - Why it's built this way - the baseline offender the rule must catch before the harder lambda and async-state-machine variants are proven.
- Where it's used - exercised by
ClockReadTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs).
IFakeExportService
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:25· Level 0 · interface
- What it is - a marker-only cross-module contract that exists purely as test data: it stands in for the kind of service one module exports and another module resolves, so the module-conformance base can be proven to reach a real DI container.
- Depends on - nothing. The whole declaration is
public interface IFakeExportService;(ModuleConformanceTestsBaseTests.cs:25), a body-less interface using the semicolon form, with a one-line doc above it (:24). - Concept introduced - the cross-module export contract, in miniature. Under the module system (IModule, ModuleLoader), a module that is switched off in ModulesSettings still has to leave behind stub registrations, otherwise every other module that resolves its exported interface fails to construct.
IFakeExportServiceis the smallest possible stand-in for such an exported interface. [Rubric §14 - Testability] assesses whether a rule can be proven with focused inputs; a marker interface is the least amount of surface that still lets the fitness base observe a realServiceDescriptor. - Walkthrough - no members. Its only role is to be the
ServiceTypethat FakeDependentModule registers a stub against (:44) and that FakeDependentModuleConformanceTests looks up in the built collection (:74). - Why it's built this way - a real cross-module contract would drag a module's whole application surface into the framework's architecture-test assembly. A local marker keeps the fixture self-contained, which is the same discipline the rule library itself follows (it matches framework types by full name rather than referencing them).
- Where it's used - implemented by DisabledFakeExportService and registered by FakeDependentModule.
TwoMemberClockFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:40· Level 0 · class
- What it is -
internal sealed class TwoMemberClockFixture(:40-:45) with two clock-reading methods,Exempted(:42) andStillReported(:44), both returningDateTime.UtcNow. - Depends on - nothing beyond
System.DateTime; a leaf fixture compiled purely so the clock-read rule has real IL to scan. - Concept - the member-level allowlist case for the clock-read rule: with two otherwise-identical members reading the same forbidden API, an allowlist entry naming only
TwoMemberClockFixture.Exemptedmust silence that one member and leaveStillReportedreported, proving the allowlist is checked per member rather than per type. - Walkthrough -
Exempted()andStillReported()are single-expression methods,=> DateTime.UtcNow;, differing only in name. - Where it's used -
MemberAllowlistEntry_ExemptsOnlyThatMember(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:71-:57) allowlists{FixtureNamespace}.{nameof(TwoMemberClockFixture)}.Exemptedand asserts the report excludesExempted(:55) while still containingStillReported(:56).
TodayReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:48· Level 0 · class
- What it is -
internal sealed class TodayReadingFixture(:48-:51) with one member,Stamp(), that returnsDateOnly.FromDateTime(DateTime.Today)(:50). Its doc comment callsDateTime.Today"the fifth ambient-clock getter" (:47). - Depends on - nothing beyond
System.DateTimeandSystem.DateOnly. - Concept - widens the clock-read rule's coverage past
UtcNow/Now:DateTime.Todayreads the ambient local clock just asDateTime.Nowdoes, so a member that only wants a date still has to be reported. Wrapping the read inDateOnly.FromDateTimealso shows the rule keys on the getter call itself, not on the member's return type. - Walkthrough -
public DateOnly Stamp() => DateOnly.FromDateTime(DateTime.Today);(:50), the whole type. - Where it's used -
DateTimeToday_IsFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:32-:35) asserts the report contains{nameof(TodayReadingFixture)}.Stamp reads DateTime.Today(:34), in the ClockReadTests suite.
UndecoratedFixtureController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:128· Level 0 · class
- What it is - a
ControllerBasefixture with a single undecorated action, the forgotten-[Authorize]shape that the undecorated-endpoint check exists to catch: no[Authorize], no[AllowAnonymous], nothing but the route (:125-:132). - Depends on - nothing beyond
ControllerBase; it is a leaf fixture with no members other than its one action. - Concept - the negative case for AnonymousEndpointTestsBaseTests's strict-mode undecorated-endpoint assertions: a controller carrying no authorization decision at all is invisible to the
[AllowAnonymous]allow-list, which is the gap the check closes (Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll,:75-:84). - Walkthrough -
public sealed class UndecoratedFixtureController : ControllerBase(:126) with one member,[HttpGet("undecorated")] public IActionResult ListAsync() => Ok();(:130-:131); its doc comment describes the action as gated only by the fallback policy (:128). - Where it's used -
Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAllasserts the strict-mode failure message names it vianameof(UndecoratedFixtureController)(:81).
UtcNowReadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ClockReadFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ClockReadFixtures/ClockReadFixtures.cs:6· Level 0 · class
- What it is -
internal sealed class UtcNowReadingFixture(:6-:9) with one member,Stamp(), that returnsDateTime.UtcNowdirectly. - Depends on - nothing beyond
System.DateTime. - Concept - the simplest positive case for the clock-read rule: a direct, unconditional read of
DateTime.UtcNowfrom an ordinary instance method, with no lambda, async state machine or injectedTimeProviderin the way. - Walkthrough -
public DateTime Stamp() => DateTime.UtcNow;(:8), the whole type. - Where it's used -
DirectReadsOfEitherClockType_AreFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:22-:29) asserts the report containsUtcNowReadingFixture.Stamp reads DateTime.UtcNow(:27).
ThinFixtureController
MMCA.Common.Architecture.Tests -
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures-MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:24(span 24-27) - Level 0 - class
- What it is -
internal sealed class ThinFixtureController(string only) : ControllerBase, the "one injected collaborator" member of the API-controller population compiled forConstructorDependencyCountTestsBaseTests(:22-:27). - Depends on - a single primary-constructor parameter,
string only, read back throughinternal string Collaborators => only;(:26) so it is never an unread parameter (CS9113). - Concept introduced - the "thin" half of a fat/thin fixture pair.
ConstructorDependencyCountTestsBaseTestsenforces a ceiling on injected collaborators per top-level type; the fixture file supplies one member at or under that ceiling and one over it for each population the gate covers (:8-:9). FatFixtureController is the three-collaborator sibling in the same population (:17-:20);ThinFixtureControlleris the counterexample that must pass whereFatFixtureControllermust fail, so the ceiling test has a known-good and a known-bad case to distinguish rather than a single sample. - Walkthrough - one constructor parameter,
only, surfaced throughCollaboratorsfor the same read-back reason every fixture in the file follows. - Why it's built this way -
[ApiController](:23) and top-level placement match exactly what the gate scans; being sealed and internal keeps the fixture out of any public surface while still being a real, compilable controller type. - Where it's used - paired against FatFixtureController as the API-controller population read by ConstructorDependencyCountTestsBaseTests.
RebuildFixtureProjectionCommand
MMCA.Common.Architecture.Tests -
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures-MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:30- Level 0 - record
- What it is -
internal sealed record RebuildFixtureProjectionCommand;(ConstructorDependencyFixtures.cs:30), a payload-free marker command in the compiled injection-shape fixtures forConstructorDependencyCountTestsBaseTests(:763in the group's type map). - Depends on - nothing; the record declares no members.
- Concept introduced - why a fitness fixture command can be deliberately empty. The class-level doc comment on the file explains the fixtures exist to give
ConstructorDependencyCountTestsBaseTestsone fat and one thin top-level member of each population the ceiling covers, API controllers, command handlers and query handlers (:8-:9). Types are TOP-LEVEL on purpose because the gate skips nested types, and every injected collaborator is read back so none is an unread primary-constructor parameter (:10-:12). This command is marked payload-free specifically so the command-validator coverage rule skips it: pointing a coverage map at this assembly still counts the same five data-carrying commands it counted before the fixture file existed (:12-:14,:29). - Walkthrough - no fields, no properties, nothing to read back; the type exists purely to give
RebuildFixtureProjectionHandlera command generic argument to close over. - Why it's built this way - a command with a payload would add itself to the validator-coverage count and skew an unrelated fitness assertion; a marker record sidesteps that without weakening the constructor-dependency fixture it exists to support.
- Where it's used - the command type of
ICommandHandler<RebuildFixtureProjectionCommand, Result>implemented by RebuildFixtureProjectionHandler (:33-:42), the "three collaborators" (fat) member of the command-handler population.
ResourceEntry
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PluralSentenceResourceTests.cs:70· Level 0 · record
- What it is -
private sealed record ResourceEntry(string Key, string Value);(:70), nested insidePluralSentenceResourceTestsand documented as "one resource entry: its key and its value" (:69). - Depends on - nothing; two positional
stringmembers. - Concept - the unit of input for the fixed-plural-sentence detector.
PluralSentenceResourceTestsfails on a.resxvalue that renders a count inside a fixed plural sentence ("{0} recipients", "{0} item(s)"), which reads wrongly for a count of one; count-sensitive messages are expected to declare.One/.Othersiblings resolved through the plural localizer extension (ADR-027) (:6-:14). Flattening every resource to a(Key, Value)pair lets the same detector run over real resource files and over hand-written inline cases. - Walkthrough -
ReadEntriesprojects each<data>element of a.resxfile intonew ResourceEntry(name, value), defaulting a missing attribute or element tostring.Empty(:72-:77).FixedPluralSentences(IReadOnlyList<ResourceEntry>)collects every key, skips a.Otherkey and the base key of a pair that declares a.Othersibling (:87-:90), then reports each value where a format placeholder is followed by a plural-marked word (:91-:94). - Where it's used - only inside the defining file: built by
ReadEntriesfor the repository scan inFrameworkResources_HaveNoCountInAFixedPluralSentence(:18-:39), and constructed inline (new(key, value)) by the detector theoriesDetector_Flags_ACountInAFixedPluralSentence(:46-:48),Detector_Accepts_AValueThatIsNotAFixedPluralSentence(:56-:58) andDetector_Accepts_TheBaseKeyOfAPluralPair_AsItsFallback(:61-:67). PluralSentenceResourceTests itself is rolled up per project.
SwitchExpressionFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainThrowFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:115· Level 0 · class
- What it is -
internal static class SwitchExpressionFixture(:115-:120) with one method,Label(int n), whose body is a switch expression with arms for1and2and no discard arm (:118). - Depends on - nothing beyond the compiler: the
#pragma warning disable CS8509, IDE0072around the method (:117,:119) is what lets an incomplete switch expression compile at all. - Concept - a false-positive guard for the domain-throw rule. For a switch expression with no discard arm the compiler writes its own default-arm throw of
SwitchExpressionExceptioninto the method; that throw is not the developer's, so the rule must stay silent about it (doc comment,:110-:114). Without this fixture an IL-level throw scan could pass every other case and still flag every non-exhaustive switch in a domain assembly. - Walkthrough -
internal static string Label(int n) => n switch { 1 => "one", 2 => "two" };(:118), the whole type. - Where it's used -
CompilerSwitchExpressionThrow_IsNotFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:72-:79) runsArchitectureRules.DomainThrowsOnlyArgumentGuardsover the fixture assembly (:73) and asserts the failure message does not containSwitchExpressionException(:76-:78), in the DomainThrowFitnessTests suite; the rule body lives in ArchitectureRules.
ReadRepositoryFixtureQuery
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ReadRepositoryFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:12· Level 0 · record
- What it is -
internal sealed record ReadRepositoryFixtureQuery;(ReadRepositoryFixtures.cs:12), the payload-free marker query shared by both query-handler fixtures the read-repository rule is proven against. - Depends on - nothing; the record declares no members.
- Concept introduced - the read-repository fitness rule.
ArchitectureRules.QueryHandlersUseReadRepositories, run throughQueryHandlerReadRepositoryTestsBase.QueryHandlers_ShouldUseReadRepositories(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15,:24-:26), reads the IL of everyIQueryHandler<,>implementation in the map's Application assemblies and asserts each one asksIUnitOfWorkforGetReadRepository, neverGetRepository: the write repository hands a query the mutation surface (add, remove, tracked reads) it must not use (:4-:8). Two handlers close over this same query to prove both directions:WriteRepositoryQueryHandlerFixturecallsunitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>()after anawait Task.Yield(), so the offending call sits inside a state-machine body (ReadRepositoryFixtures.cs:17-:26), andReadRepositoryQueryHandlerFixturecallsunitOfWork.GetReadRepository<ReadRepositoryFixtureAggregate, int>()directly, the sanctioned shape (:29-:37). [Rubric §34 - Architecture Governance & Documentation] covers this fitness function; the async-state-machine offender in particular pins the rule against a violation that source review alone tends to miss. - Walkthrough - no fields, no properties; the type exists purely to close over a query generic argument for both handler fixtures.
- Why it's built this way - one query shared by both handlers keeps the fixture pair a controlled minimal-difference comparison: the only variable between them is which repository accessor is called.
- Where it's used - the query type of
IQueryHandler<ReadRepositoryFixtureQuery, Result<int>>implemented by bothWriteRepositoryQueryHandlerFixtureandReadRepositoryQueryHandlerFixture(ReadRepositoryFixtures.cs:16-:37), which close overReadRepositoryFixtureAggregate(:10) as the repository's entity type argument.
PasswordProbe
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PasswordRuleParityTests.cs:54· Level 0 · record
- What it is -
private sealed record PasswordProbe(string Password);nested insidePasswordRuleParityTests(PasswordRuleParityTests.cs:54), documented as "the smallest model the server rule can be bound to" (:53). It gives both the client attribute and the server validator a real object to evaluate one password against. - Depends on - nothing; a positional record with one
stringproperty. - Concept introduced - client/server validation parity. The Register form validates a password with PasswordComplexityAttribute while the API validates it with StrongPasswordRules<T>; if the two disagree, the form either rejects a password the API would accept or lets one through that the API refuses after a round trip (
PasswordRuleParityTests.cs:7-:12). The test lives in the architecture test project because it is the one project referencing both sides: the UI test project references MMCA.Common.UI only, and UI may depend on Shared alone (:14-:15). [Rubric §24 - Forms, Validation & UX Safety] assesses whether client-side validation matches the server's verdict; this probe is what lets a single theory compare the two verdicts directly rather than trusting they were written alike. - Walkthrough - the theory
ClientPasswordComplexity_MatchesServerStrongPasswordRulesbuilds one probe per row (:38), then computes the client verdict the way anEditFormdoes, callingGetValidationResultwith aValidationContext(probe)whoseMemberNameisnameof(PasswordProbe.Password)(:41-:43), and the server verdict by bindingnew StrongPasswordRules<PasswordProbe>(static p => p.Password)and callingValidate(probe).IsValid(:44-:46). The two must be equal (:48-:50). The twelveInlineDatarows cover ASCII valid and invalid cases plus accented, umlaut, CJK and full-width-digit inputs (:24-:35), written as escapes so the source stays ASCII (:16-:18). - Why it's built this way - the server rule is a FluentValidation validator over a model, so it needs a type with a property selector to bind to; a private nested record is the least surface that satisfies that without borrowing a real request DTO.
- Where it's used - only inside
PasswordRuleParityTests(no usages outside the defining file).
ReadRepositoryFixtureCommand
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ReadRepositoryFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:14· Level 0 · record
- What it is -
internal sealed record ReadRepositoryFixtureCommand;(ReadRepositoryFixtures.cs:14), a payload-free marker command that gives WriteRepositoryCommandHandlerFixture a generic argument; it exists so a command handler using the write repository sits in the fixture set as the out-of-scope control case. - Depends on - nothing; the record declares no members.
- Concept introduced - cross-reference to the compiled-fixture pattern introduced under InjectedClockFixture: the class-level comment on this file states the read-repository rule reads IL, so its behavior is pinned against real handler bodies rather than executed (
ReadRepositoryFixtures.cs:8-:9; none of the fixtures is ever run). - Walkthrough - no fields, no properties; the type exists purely to close over a command generic argument.
- Why it's built this way -
QueryHandlerReadRepositoryTestsBase.QueryHandlers_ShouldUseReadRepositories(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15,:24-:26) only walksIQueryHandler<,>implementations; a command handler on the write repository is deliberately out of scope, so this fixture proves the rule leaves it alone. - Where it's used - the command type of
ICommandHandler<ReadRepositoryFixtureCommand, Result>implemented byWriteRepositoryCommandHandlerFixture(ReadRepositoryFixtures.cs:40-:49).
CustomExceptionThrowingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainThrowFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67· Level 1 · class
- What it is - a one-method static fixture that throws the file's custom domain exception, so the domain-throw rule can be shown to judge by constructed type rather than by a list of framework exception names.
- Depends on - TicketDomainException (
DomainThrowFixtures.cs:69). - Concept - cross-references the domain-throw concept introduced by its sibling fixtures. What this one adds is the discrimination that matters most in practice: teams that adopt the Result pattern often keep a
DomainExceptionhierarchy as a compromise, and a rule built around a deny-list of BCL exception names would wave it through. The rule instead allows exactly three types and reports everything else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:19-:21,:132-:135), so a custom exception is caught for what it is. The fixture doc puts it in one sentence: the same defect wearing a domain name (:48). - Walkthrough - the whole type is
internal static class CustomExceptionThrowingFixture(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainThrowFixtures/DomainThrowFixtures.cs:67) with one member,internal static void Reject() => throw new TicketDomainException("The ticket was rejected");(:69). Thenewobjimmediately precedes thethrow, soConstructedExceptionTypenames it and the site is a hard violation rather than an UNVERIFIABLE. - Where it's used - the subject of
CustomDomainException_IsAlsoFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:49-:57), and the residual offender the allowlist facts assert is still reported when only its sibling is exempted (:114-:126).
AnonymousPage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:74· Level 1 · class
- What it is -
public sealed class AnonymousPage;(RouteAuthorizationTestsBaseTests.cs:74), a body-less fixture page carrying[Route("/fixture/anonymous")](:73) and no[Authorize]attribute at all. - Depends on - nothing; only the
Routeattribute fromMicrosoft.AspNetCore.Components(:2). - Concept - the "no authorize attribute" negative case for the role check.
RouteAuthorizationTestsBase.RequiresRoleanswers whether any of a page's own or inheritedAuthorizeAttributes names exactly the required role (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:86-:91); a page with no attribute has nothing to match, so the answer must befalse. - Walkthrough - no members. It is the last
[InlineData(typeof(AnonymousPage), false)]row ofRequiresRole_ReadsEveryAuthorizeAttribute(:25-:27), which callsRoleProbe.Requires(page, AdminRole)and asserts the expected boolean. - Where it's used - only inside the defining file, as that theory row (
:25). It is not a governed page of eitherStackedAttributeTests(:93-:96) or the missing-role subclass.
BareOnlyPage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:66· Level 1 · class
- What it is -
public sealed class BareOnlyPage;(RouteAuthorizationTestsBaseTests.cs:66), a body-less fixture page with[Route("/fixture/bare-only")]and a bare[Authorize](:64-:65), so it requires a signed-in user but names no role. - Depends on - nothing;
AuthorizefromMicrosoft.AspNetCore.Authorization(:1). - Concept - a bare
[Authorize]is not a role gate.RequiresRolecompares each attribute'sRolesproperty to the required role with ordinal equality (RouteAuthorizationTestsBase.cs:86-:91), so an attribute withRolesunset never satisfies it. This page pins that boundary, and its siblingBareOverRoleGatedPage(:60-:62) shows the opposite: a bare attribute on a derived page does not hide the inherited[Authorize(Roles = "Admin")]ofRoleGatedPage. - Walkthrough - no members. It is the
[InlineData(typeof(BareOnlyPage), false)]row ofRequiresRole_ReadsEveryAuthorizeAttribute(:23,:26-:27), expected to report no role requirement. - Where it's used - only inside the defining file, as that theory row (
:23).
CreateTicketCommandValidator, UpdateTicketRequestValidator
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures· (see per-type table) · Level 1 · class
Two one-line FluentValidation validators, identical in shape and different only in what they validate. Between them they supply both forms of coverage the command-validation rule accepts: a validator for the command itself, and a validator for the request a bridged command embeds.
- Depends on -
FluentValidation.AbstractValidator<T>(CommandValidatorFixtures.cs:1) and the record each one validates. - Concept introduced - direct coverage versus bridge coverage, and why the bridge needs both halves. A command is covered when a concrete
IValidator<TCommand>exists for it, or when the command implementsICommandWithRequest<TRequest>and a concreteIValidator<TRequest>exists (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:112-:115). The second condition insists on both halves for a reason the rule doc spells out (:23-:29): CommandRequestValidator<TCommand, TRequest> is auto-registered for every bridged command, so a naive check for "is a validator registered" would always say yes, while the bridge adds no rule at all when no request validator resolves. Half a bridge is a validator that validates nothing. The rule also reads commands and validators from the same place the framework'sAddValidatorsFromAssemblyscan looks, the per-module Application assemblies (:32-:36), because a validator living anywhere else would not resolve at run time either. [Rubric §24 - Forms/Validation/UX Safety] and [Rubric §6 - CQRS & Event-Driven] are the properties; [Rubric §14 - Testability] covers the fixtures. - Walkthrough - both are
internal sealed, both declare aninternalconstructor with a single expression-bodied rule, and both useNotEmpty()on the one string property. Nothing about the rule content matters to the fitness function; the existence of a closedIValidator<T>implementation is the whole signal.
| Type | File:Line | What it covers |
|---|---|---|
CreateTicketCommandValidator |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:16 |
AbstractValidator<CreateTicketCommand> with RuleFor(c => c.Title).NotEmpty() (:18). Direct coverage: the validator names the command itself. |
UpdateTicketRequestValidator |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:33 |
AbstractValidator<UpdateTicketRequest> with RuleFor(r => r.Title).NotEmpty() (:35). Bridge coverage: it names the request, and UpdateTicketCommand is covered only through it. |
- Why they're built this way - having exactly one validator on each side of the bridge is what lets a single rule execution produce a positive result for both coverage forms while ReopenTicketCommand, whose request deliberately has none, still fails. Adding a second rule to either validator would prove nothing extra.
- Caveats / not-in-source - the rule's own limits section records two behaviors these fixtures do not exercise: a validator declared for a base command type does not count for its derived commands (matching FluentValidation's closed-type resolution), and abstract validators are ignored because nothing registers them (
ArchitectureRules.CommandValidators.cs:37-:41). - Where they're used - discovered by
ValidatedTypes(ArchitectureRules.CommandValidators.cs:105-:109) during every fact of CommandValidatorCoverageFitnessTests.
UpdateTicketRequestValidator
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:33· Level 1 · class
- What it is - the validator that makes ReopenTicketCommand, UpdateTicketCommand's bridge half a real hit: an
AbstractValidator<UpdateTicketRequest>for the requestUpdateTicketCommandwraps, not for the command itself. - Depends on - FluentValidation's
AbstractValidator<T>(:1,:33) andUpdateTicketRequest(:28), the record it validates. - Concept introduced - the request bridge only counts if a validator actually resolves for the wrapped request type.
CommandRequestValidatorreaches the command's request throughICommandWithRequest<out TRequest>and asks whetherValidatedTypescontains it; this class is what makes that lookup non-empty forUpdateTicketRequest, soUpdateTicketCommandreads as covered even though it declares no validator of its own. ReopenTicketCommand is the same shape with no validator at all, and the pair only proves the rule because they differ in exactly this one respect. - Walkthrough - one rule,
RuleFor(r => r.Title).NotEmpty()(:35), applied through the constructor. - Where it's used - resolved by name in
CommandCoveredThroughTheRequestBridge_IsCovered(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:44-:53), which assertsValidatedTypescontainsUpdateTicketRequestand thatUpdateTicketCommandis reported covered.
NoModuleAssemblies, NoOutboxSignal
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain· (see per-type table) · Level 1 · class
Two private, minimal interface stand-ins that let DeleteBehaviorConventionTests build a real EF model without wiring the framework's full DI graph.
- Depends on - IEntityConfigurationAssemblyProvider and IOutboxSignal respectively (
DeleteBehaviorConventionTests.cs:102,:135). - Concept - cross-references the minimal-DI-graph pattern this fixture-only DbContext test shares with its siblings NoEntityDataSources and NoDomainEventDispatcher: a context built only to inspect its finalized model has no need for real module assemblies or a live outbox signal, so it satisfies the two interfaces with the smallest possible bodies rather than resolving the production implementations.
- Walkthrough -
NoModuleAssembliesimplementsGetConfigurationAssemblies()by returning an empty array (:102-:105), so the model built from it carries only the framework's own tables, not any module's.NoOutboxSignalimplements bothIOutboxSignalmembers as no-ops:Signal()does nothing (:135-:139, with a comment stating why: this context is built for its model and never saves) andWaitAsyncreturnsTask.CompletedTask(:148). - Where it's used -
NoModuleAssembliesis reused outside its defining file by RestrictDeleteByDefaultConventionTests and EFRawSqlQueryExecutorTests (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Conventions/RestrictDeleteByDefaultConventionTests.cs,MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRawSqlQueryExecutorTests.cs), each twice;NoOutboxSignalis used only within its own defining file, wired intoDomainEventSaveChangesInterceptor's constructor (DeleteBehaviorConventionTests.cs:95).
| Type | File:Line | Interface implemented |
|---|---|---|
NoModuleAssemblies |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:102 |
IEntityConfigurationAssemblyProvider, empty assembly list. |
NoOutboxSignal |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:135 |
IOutboxSignal, both members no-op. |
NoServices
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs:59· Level 1 · class
- What it is - a private, sealed negative-control subclass nested inside MessageBusBackpressureTestsBaseTests. It derives from
MessageBusBackpressureTestsBaseand overrides the one abstract member,Services, with an empty list (MessageBusBackpressureTestsBaseTests.cs:59-:62). - Depends on -
MessageBusBackpressureTestsBase, whose abstractIReadOnlyList<string> Services(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:52) names the service appsettings files the gate checks. - Concept - a gate that checks nothing must fail, not pass. The base's single fact opens by asserting
Servicesis not empty (MessageBusBackpressureTestsBase.cs:70-:72) before it loops over the services. A consumer that forgot to list any service, or a refactor that emptied the list, would otherwise get a vacuously green broker-backpressure gate.NoServicesis the minimal subclass that exercises that guard. - Walkthrough - the outer test
EmptyServiceList_FailsRatherThanCheckingNothinginstantiatesNoServicesand takes its inherited factEveryServiceAppSettings_BoundsBrokerBackpressureas a delegate, then asserts it throws with the reason "a gate that lists no service checks nothing" (MessageBusBackpressureTestsBaseTests.cs:51-:57). Invoking the fact on a hand-built instance, rather than letting xUnit discover it, is what lets a failing fact be asserted on instead of failing the run. - Where it's used - only inside its defining file; it has no consumers outside MessageBusBackpressureTestsBaseTests.
FixtureGoodFeatures
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.FeatureFlagFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:11· Level 1 · class
- What it is - the negative-control half of the feature-flag lifecycle fixture set: a static class holding two constants that are declared exactly right, one
Permanentand oneTemporarywith aRemoveByfar enough in the future to stay green (:14-:19). Nothing points the framework's ownFeatureFlagLifecycleTestsat this assembly; it scansMMCA.Common.Sharedinstead (class doc,:6-:9). - Depends on -
[FeatureFlag](MMCA.Common.Shared.FeatureFlags,:1,:14,:18), the same attribute ArchitectureRules's lifecycle rules read. - Concept introduced - a rule that only ever fails proves nothing about the passing case. Its siblings
FixtureBadFeaturesandFixtureExpiredFeatureseach carry one deliberate violation, but a rule tuned to fire on every input would also fire here, soFixtureGoodFeaturesis what lets the adversarial tests assert absence: the declaration rule's failure message must NOT name either of its two constants, and the expiry rule's failure message must not nameLiveTemporaryFlageven though it is Temporary, because its window has not closed. [Rubric §14 - Testability] is the property; ADR-031 is the lifecycle policy under test. - Walkthrough -
PermanentFlag = "Fixture.Permanent"(:15), tagged[FeatureFlag(FeatureFlagLifetime.Permanent, Owner = "Fixtures")];LiveTemporaryFlag = "Fixture.LiveTemporary"(:19), tagged[FeatureFlag(FeatureFlagLifetime.Temporary, RemoveBy = "2099-12-31", Owner = "Fixtures")]. Both are the shape the declaration rule requires, and the far-future date is what keeps the second one out of the expiry rule's reach. - Where it's used - asserted absent from
DeclarationRule_LeavesCorrectlyDeclaredFlagsAlone's message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:60-:67) and fromExpiryRule_FlagsATemporaryFlagPastItsRemoveBy_NamingTheDateAndTheOwner's message (:70-:82).
InheritingFitnessController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:81· Level 1 · class
- What it is - a body-less concrete controller that inherits
AbstractFitnessControllerBaseand, with it, the base's declared POST action. It is the fixture for the "covered by inheritance" branch of the idempotency gate. - Depends on - AbstractFitnessControllerBase (
public sealed class InheritingFitnessController : AbstractFitnessControllerBase;,IdempotencyFitnessTests.cs:81). - Concept introduced - inherited attributes are the reason a base-class action does not have to be redeclared. Reflection over a derived type reports the base's
CreateInheritedAsyncas one of its public methods, and the rule reads attributes withinherit: true(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:80-:81), so the[Idempotent]sitting on the base action covers every subclass. That is not a detail of the fixture: in the real framework, a concrete controller deriving fromAuthControllerBaseorAggregateRootEntityControllerBasesatisfies the gate through those bases (ArchitectureRules.Idempotency.cs:31-:34), which is the only reason the rule is not a wall of duplicate annotations. This fixture is what pins that behavior. [Rubric §9 - API & Contract Design] and [Rubric §14 - Testability] apply, as for its sibling fixtures. - Walkthrough - no members at all; the declaration ends at its semicolon (
:81). Being concrete is what puts it into the rule'sConcreteClassesscan while its abstract base stays out. - Where it's used - asserted absent from the rule's failure message by
Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations(IdempotencyFitnessTests.cs:34-:36).
InheritingFixtureController
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:157· Level 1 · class
- What it is - a body-less concrete controller deriving AbstractAnonymousFixtureControllerBase. It is the fixture for the ruling that an inherited
[AllowAnonymous]is reported once, at the base that declares it, and never again on the derived type. - Depends on - AbstractAnonymousFixtureControllerBase (
public sealed class InheritingFixtureController : AbstractAnonymousFixtureControllerBase;,AnonymousEndpointTestsBaseTests.cs:157). - Concept introduced - the same inheritance question, answered the opposite way from the idempotency gate. InheritingFitnessController exists because the idempotency rule reads attributes with
inherit: true, so a base declaration covers every subclass. The anonymous-endpoint scan does the reverse: it reads methods withBindingFlags.DeclaredOnlyand attributes withinherit: false(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:264,:146). Both choices are correct for their own rule, and the reason is who writes the allow-list. Idempotency intent is declared by the framework and must flow down to consumers, so inherited reads are what stop a wall of duplicate annotations. An anonymous endpoint must be reviewed, and if the scan reported the framework's base action once per derived controller, every consumer repo would have to re-approve the framework's six credential-exchange endpoints under its own type names, forever. The base's own comment states exactly that (:140-:141). [Rubric §11 - Security] is the property; [Rubric §33 - Developer Experience] is the reason for the shape, since a gate that demands the same approval in every downstream repo gets rubber-stamped rather than read. - Walkthrough - no members; the declaration ends at its semicolon (
:94). Being concrete puts it in the scan (IsControllerwalks the base chain,AnonymousEndpointTestsBase.cs:159-:112), and theDeclaredOnlyfilter is what keeps it silent: it declares no methods of its own, soAnonymousEndpointsOfyields nothing for it. - Where it's used - the subject of
Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController(AnonymousEndpointTestsBaseTests.cs:61-:71), which reads ConformantTests's scan output and asserts it does not contain{InheritingFixtureController.FullName}.InheritedAnonymousAsync(:69-:70). The inline comment there names the consequence being prevented (:64-:66).
HighFloorTemplate
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:59· Level 1 · class
- What it is - a
private sealednested subclass of CostTagConventionTestsBase insideCostTagConventionTestsBaseTests(:59-:66). It supplies the three abstract members:MinimumContainerApps => 3(:61),DatabaseNamePrefix => "Fixture_"(:63) andBicepResource => "fixtures.costtag-main.bicep"(:65). - Depends on -
CostTagConventionTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36, the abstractMinimumContainerApps) and the embedded templatefixtures.costtag-main.bicep, the same one the outer class points at (CostTagConventionTestsBaseTests.cs:17). - Concept - the floor-guard negative case. The outer class declares a floor of 2 (
:13) against that template and passes; this subclass raises the floor to 3 against the same template, so the base discovers fewer container apps than demanded. The test's own reason string says why that must fail: discovering fewer apps than the floor means the parse anchor drifted, so the gate must not pass vacuously (:37-:38). - Walkthrough - nothing but the three property overrides; the only difference from the passing configuration is the
3against2. Beingprivate, it is invisible to xUnit's discovery, so only the test that instantiates it runs it. - Where it's used -
TooFewContainerApps_FailsTheFloor(CostTagConventionTestsBaseTests.cs:33-:39) buildsnew HighFloorTemplate().EveryContainerApp_CarriesAServiceTagand asserts it throws. No usages outside the defining file.
FixtureAiConsumer
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AiBoundaryFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AiBoundaryFixtures/FixtureAiConsumer.cs:9· Level 1 · class
- What it is - a public static fixture class whose one job is to name a governed
MMCA.Common.AItype from inside the architecture test assembly, so a map that treats that assembly as a non-Infrastructure layer must failGovernedAiPackageStaysBehindInfrastructure(class doc,FixtureAiConsumer.cs:5-:8). - Depends on - PromptContract (
using MMCA.Common.AI;,FixtureAiConsumer.cs:1; parameter type at:14). - Concept introduced - a boundary rule needs a real violator to prove it matches anything.
ArchitectureRules.GovernedAiPackageStaysBehindInfrastructurewalks every layer of the map whose assembly is not at the model boundary and asserts its types have no dependency onGovernedAiPackageNamespace(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:75-:90), where that constant is the undotted"MMCA.Common.AI"(:25). An assembly counts as the model boundary when its name contains.Infrastructure, equalsMMCA.Common.AI, or starts with the governed package prefix (:99-:106);MMCA.Common.Architecture.Testsis none of those, so a type in it that namesPromptContractis a genuine violation. The test class doc records why the fixture exists: the rule once matched a dotted entry that NetArchTest never matches, because it compares whole namespace segments, so the gate passed vacuously in every subclass (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:7-:12). [Rubric §14 - Testability] is the property: the fixture keeps AiDependencyIsolationTestsBase, which calls the same rule, from going green by scanning nothing. - Walkthrough - one member,
NameOf(PromptContract contract)(FixtureAiConsumer.cs:14-:18): it guards the argument withArgumentNullException.ThrowIfNulland returnscontract.Name. The body is deliberately trivial; reading the contract's name "is all it takes to depend on the package" (:11), and a parameter typed as a governed type is the dependency the rule must see. - Where it's used - only by AiBoundaryRulesTests.
ALayerNamingAGovernedAiType_FailsTheRulepassestypeof(FixtureAiConsumer).Assemblyinto a private one-layer ArchitectureMapBase subclass,SingleLayerMap, that declares the assembly as the framework's Application layer (AiBoundaryRulesTests.cs:18-:19,:36-:41), then asserts the failure message contains"must not reference MMCA.Common.AI"andnameof(FixtureAiConsumer)(:21-:23). Its sibling fact runs the same map over theMMCA.Common.APIassembly and asserts no throw, pinning thatAPIis not mistaken for theAIsegment (:26-:33).
FixtureBadFeatures, FixtureExpiredFeatures, FixtureGoodFeatures
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.FeatureFlagFixtures· (see per-type table) · Level 1 · class
Three static fixture classes carrying [FeatureFlag]-annotated constants for FeatureFlagLifecycleTests: one class of constants that must pass the lifecycle rule, one of constants that must each fail it for a distinct reason, and one that is expired.
- Depends on - FeatureFlagAttribute and FeatureFlagLifetime (
MMCA.Common.Shared.FeatureFlags,FeatureFlagFixtures.cs:11-:41). - Concept - cross-references the flag-lifecycle concept introduced by FeatureFlagLifecycleTestsBase (
Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10): every flag constant must declare[FeatureFlag(Permanent|Temporary, RemoveBy, Owner)], and a temporary flag past itsRemoveBydate fails the build (ADR-031). These three classes are the fixture matrixFeatureFlagLifecycleTestsruns the rule against: one constant per violation the rule is meant to catch, plus the shapes that must stay silent. - Walkthrough -
FixtureGoodFeatures(:11-:20) has two constants that must pass:PermanentFlag, declaredPermanentwith noRemoveBy(:13-:14), andLiveTemporaryFlag,Temporarywith aRemoveByfar enough out to stay green,2099-12-31(:16-:18).FixtureBadFeatures(:23-:41) has four constants that must each fail for a different reason:UnannotatedFlagcarries no[FeatureFlag]at all (:28-:29);PermanentWithRemoveByFlagisPermanentyet still names aRemoveBydate, a contradiction (:31-:33);TemporaryWithoutRemoveByFlagisTemporarywith noRemoveBy, which the rule treats as permanent in everything but name (:35-:37);TemporaryWithUnparseableRemoveByFlagisTemporarywith aRemoveByin an order the rule cannot parse,"31/12/2030"(:39-:41).FixtureExpiredFeatures(:42-:47) has one constant,ExpiredFlag,TemporarywithRemoveBy = "2020-01-01", which is what the expiry check exists to catch (:44-:46). - Why it's built this way - splitting the matrix into good/bad/expired classes rather than one flat list lets each
FeatureFlagLifecycleTestsfact read only the constants relevant to it, so adding a new failure mode toFixtureBadFeaturescannot accidentally break a fact that only readsFixtureGoodFeatures.
| Type | File:Line | Role |
|---|---|---|
FixtureGoodFeatures |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:11 |
Two constants that must pass the lifecycle rule. |
FixtureBadFeatures |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:23 |
Four constants, each failing for a distinct reason (unannotated, contradictory, dateless-temporary, unparseable date). |
FixtureExpiredFeatures |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/FeatureFlagFixtures/FeatureFlagFixtures.cs:42 |
One constant past its RemoveBy date. |
- Where it's used - all three are read across multiple facts of
FeatureFlagLifecycleTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs), which asserts each fixture's constants land in the rule's expected pass/fail buckets.
FixtureDomainEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:13· Level 1 · record
- What it is - the single throwaway domain event every handler fixture in
DomainEventSaveFixturesis generic over. It carries no meaning; it exists so the four fixture handlers can implement a real, closedIDomainEventHandler<T>. - Depends on - IDomainEvent (
public sealed record FixtureDomainEvent(DateTime DateOccurred, Guid MessageId) : IDomainEvent;,DomainEventSaveFixtures.cs:13, with the interface import at:3). - Concept - cross-references the handler-save concept introduced by IBadgeGranter. Its own contribution is the closure: the rule finds handlers by asking CallGraphIndex whether a type implements the open interface
MMCA.Common.Application.Interfaces.IDomainEventHandler1matched by full name with Cecil's arity marker (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:8-:9,:96`), so any closed instantiation works and one event type is enough for the whole fixture set. - Walkthrough - two positional members,
DateTime DateOccurredandGuid MessageId, which are the two members IDomainEvent requires. It is the onlypublictype in the file; the handlers around it are allinternal. - Where it's used - the type argument of DirectSavingHandler, TransitiveSavingHandler, InterfaceDispatchSavingHandler and InnocentHandler, and the assembly anchor of DomainEventHandlerSaveFitnessTests's FixtureAssemblyMap (
DomainEventHandlerSaveFitnessTests.cs:116).
FixtureExcludedShape
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:50· Level 1 · class
- What it is - the exclusion control among the compiled IL fixtures for the substitutability rule: an
internal sealedsubclass ofFixtureShapewhoseCorners()override throwsNotSupportedException("Excluded by name in the test.")(SubstitutabilityFixtures.cs:50-:53). It refuses an abstract member exactly like FixtureRefusingShape, but the test lists that member as reviewed. - Depends on -
FixtureShape, the abstract base declared in the same file (:9-:14, abstractCorners()at:11), andArchitectureRules.SubstitutableMembersDoNotRefuseTheContract, which takes aFrozenSet<string>of excluded members (SubstitutabilityRuleTests.cs:25). - Concept - an exclusion list silences exactly the member it names. The rule reads IL, so the refusal has to be real compiled code.
ExcludedMember_IsNotFlaggedbuilds the key{FullName}.Corners(SubstitutabilityRuleTests.cs:18), proves the rule reports it with an empty exclusion set (:58-:60), then proves it disappears when the set names it while FixtureRefusingShape'sDescribeis still reported (:62-:69). - Walkthrough - one override,
Corners() => throw new NotSupportedException(...); no other members. - Where it's used - only
SubstitutabilityRuleTests.cs:18(the excluded-member key), through the fixture assembly map at:73-:81.
FixtureRefusingShape
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:17· Level 1 · class
- What it is - the positive control for an override that refuses its base: an
internal sealedsubclass ofFixtureShapewhoseCorners()returns4(honoured) and whoseDescribe()throwsNotSupportedException("A refusing shape cannot describe itself.")(SubstitutabilityFixtures.cs:17-:22). - Depends on -
FixtureShape(:9-:14), whose virtualDescribe()returns"shape"(:13), so the override really does replace a working default with a refusal. - Concept - only the refusal is reported, not the whole type. Because
Corners()is honoured andDescribe()is not, the assertionmessage.Should().Contain("FixtureRefusingShape.Describe")(SubstitutabilityRuleTests.cs:29) pins that the rule flags the member and names the exception,System.NotSupportedException(:30). - Walkthrough - two overrides, one conforming (
:19) and one throwing (:21). - Where it's used -
SubstitutabilityRuleTests.cs:29and:69(it must still be reported after the exclusion) and:79, where its assembly anchors the fixture map's Domain layer.
FixtureUnfinishedExporter
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.SubstitutabilityFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:31· Level 1 · class
- What it is - the positive control for an interface member left as a placeholder: an
internal sealedimplementation ofIFixtureExporter(:25-:28) whoseExport()throwsNotImplementedException(:31-:34). - Depends on -
IFixtureExporter, the one-member interface declared in the same file (:25). - Concept - the rule covers interface implementations as well as overrides.
InterfaceImplementationThrowingNotImplemented_IsFlaggedasserts the report namesFixtureUnfinishedExporter.ExportandSystem.NotImplementedException(SubstitutabilityRuleTests.cs:40-:41). The negative counterpart isFixtureStandaloneThrower.Load(SubstitutabilityFixtures.cs:40-:44), which throws but overrides and implements nothing, so no caller was promised it and the rule must not flag it (SubstitutabilityRuleTests.cs:45-:53). - Walkthrough - a single expression-bodied member.
- Where it's used - only
SubstitutabilityRuleTests.cs:40(the name assertion).
EditorRequiredParameterConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/EditorRequiredParameterConventionTests.cs:26· Level 1 · class
- What it is - a framework-only component-contract gate (rubric section 18, per the class doc at
:5-:9): every public[Parameter]on a publicMMCA.Common.UIcomponent whose type is a non-nullable reference type must carry[EditorRequired], so the Razor compiler flags a call site that omits it (RZ2012) instead of the component meetingnullat render time. - Depends on - UISharedAssemblyReference (the reflection anchor for the shared UI assembly,
EditorRequiredParameterConventionTests.cs:104), plus externals: ASP.NET CoreIComponent,ParameterAttribute,EditorRequiredAttribute,CascadingParameterAttribute,SupplyParameterFromQueryAttribute,RenderFragmentandRenderFragment<T>(:1,:105-:109,:119-:120),System.Reflection.NullabilityInfoContext(:72,:125), xUnit[Fact]and AwesomeAssertions. - Concept introduced - nullability annotations as a component contract. A non-nullable reference parameter is a promise that the caller supplies a value;
[EditorRequired]is what turns that promise into a compile-time diagnostic at the call site. Reflection reads the nullability the compiler emitted, so the gate can find every parameter whose type says "never null" but whose attributes do not ask the caller for it. What reflection cannot see is a property initializer, so a parameter that is genuinely optional because its initializer supplies a real default is listed by name with the default it relies on; an initializer that only guards against null (string.Empty,[]) is not a default and is not listed (class doc,:17-:23). [Rubric §18 - UI Architecture] is the category the class doc names; [Rubric §14 - Testability] applies because the gate also proves its own scan and allow-list stay live. - Walkthrough - one private set and three
[Fact]s.ParametersWithInitializerDefaults(:31-:62) is an ordinalHashSet<string>of tenComponent.Propertykeys, each preceded by a comment naming its default:ApiFileDownloadButton.ContentType("application/octet-stream"),ApiFileDownloadButton.HttpClientName("APIClient"),ApiFileDownloadButton.Icon,ClickableCard.Class,EmptyState.Icon,ErrorSummary.Class,MmcaThemeProviders.Theme(MMCATheme.Instance), theEmptyIconofMobileCardList1andMobileInfiniteScrollList1, andUserAdminList1.AssignableRoles` (an empty list). Generic components appear under their reflection names with the arity marker.Components_AreDiscovered_GateIsNotVacuous(:64-:67) asserts the scan finds at least one parameter, so a drifted anchor cannot let the gate pass having scanned nothing.NonNullableReferenceParameters_ShouldBe_EditorRequired(:69-:86) keeps every discovered parameter that is a non-nullable reference type and lacks[EditorRequired](read withinherit: false), keys it asDeclaringType.Name.PropertyName, drops the allow-listed keys, orders the rest ordinally and asserts the list is empty; the failure message names the three remedies (add[EditorRequired], make it nullable, or list it with its initializer default) and every offender.InitializerDefaultAllowList_HasNoStaleEntries(:88-:97) asserts every allow-list key still names a live parameter, since an entry for a parameter that no longer exists hides nothing.- Two private helpers.
ComponentParameters(:103-:109) takes the public classes in the shared UI assembly that implementIComponent, their public instance properties declared on the type itself (BindingFlags.DeclaredOnly), and keeps those marked[Parameter]but not[CascadingParameter]or[SupplyParameterFromQuery].IsNonNullableReference(:111-:126) excludes value types, generic parameters andRenderFragment/RenderFragment<T>, then returns whetherNullabilityInfoContext.Create(property).WriteStateisNotNull.
- Why it's built this way - the scope exclusions are each a ruling in the class doc (
:10-:15): value types cannot be null; an absentRenderFragmentorEventCallbacklegitimately means "nothing to render / nobody listening" (EventCallbackis a struct, so the value-type test already drops it); an unconstrained generic parameter's nullability belongs to the caller's type argument; cascading and query-supplied members are supplied by the framework, not the caller. The doc also names two initializer guards it refuses to exempt,PageHeader.Title(an empty level-one heading) andMobileCardList.Items(the list's data), as gaps rather than exemptions (:22-:23). The stale-entry fact is what keeps the allow-list from outliving the components it excuses. - Where it's used - an independent class in the
MMCA.Common.Architecture.Testssuite with no usages outside its defining file. It guards only the framework's own shared UI assembly, so it has no Store or ADC counterpart.
DisabledFakeExportService
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:28· Level 1 · class
- What it is - the stub implementation a disabled FakeDependentModule leaves behind, so the fixture models the real "module off, contract still resolvable" behavior rather than describing it.
- Depends on - IFakeExportService (
public sealed class DisabledFakeExportService : IFakeExportService;,ModuleConformanceTestsBaseTests.cs:28). - Concept - cross-references the disabled-stub concept introduced by IFakeExportService. The pairing matters: a stub type distinct from the real implementation is what lets an assertion prove which implementation the container holds, not merely that the service type is registered.
- Walkthrough - no members; a body-less sealed class with its doc comment at
:27. It is the exact type FakeDependentModuleConformanceTests asserts on viadescriptor.ImplementationType.Should().Be<DisabledFakeExportService>()(:76). - Where it's used - registered as a singleton by
FakeDependentModule.RegisterDisabledStubs(:43-:44).
ReopenTicketCommand, UpdateTicketCommand
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures· (see per-type table) · Level 1 · record
Two bridged commands: each is a one-property record that wraps a request record and implements ICommandWithRequest<out TRequest>. They differ in exactly one thing, whether the wrapped request has a validator, and that single difference is what proves the bridge is checked on both halves.
- Depends on - ICommandWithRequest<out TRequest> (
CommandValidatorFixtures.cs:2) and the request record each wraps. - Concept - cross-references the coverage concept introduced by CreateTicketCommandValidator and UpdateTicketRequestValidator. The pair is the negative-control design that makes the bridge check meaningful: both commands are structurally identical to the rule's discovery pass (both are closed types with a settable property, both are handled, both implement the bridge marker), so the only variable left is whether
ValidatedTypescontains their request. One passes, one fails, and nothing else about them can explain the difference. - Walkthrough - the rule reaches the request type by projecting the command through the closed generic arguments of
ICommandWithRequest1matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:5,:114), then asks whether any of those arguments is in the validated set. Matching the interface by name rather than by reference is what keeps the rule package free of a compile dependency on the framework's Application layer (:117-:120`).
| Type | File:Line | The case it models |
|---|---|---|
UpdateTicketCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:31 |
record UpdateTicketCommand(UpdateTicketRequest Request) : ICommandWithRequest<UpdateTicketRequest>. No validator of its own, but its request has one, so the bridge covers it and the rule must stay silent. |
ReopenTicketCommand |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:51 |
record ReopenTicketCommand(ReopenTicketRequest Request) : ICommandWithRequest<ReopenTicketRequest>. Implements the bridge marker, but its request has no validator, so CommandRequestValidator resolves nothing and adds no rule. Its doc states the ruling: half a bridge is not coverage (:47-:50). |
- Why they're built this way - the half-bridge is the failure mode a validation-coverage rule is most likely to miss, because the registration exists and only the resolution is empty. Compiling both halves side by side is what turns that from a code-review concern into an assertion.
- Where they're used - named by
nameofinCommandCoveredThroughTheRequestBridge_IsCoveredandBridgeWithNoRequestValidator_IsNotCoverage(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:44-:64), and handled by ReopenTicketHandler and UpdateTicketHandler.
SampleDeploymentObservabilityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12· Level 1 · class
- What it is - a subclass of ObservabilityConventionTestsBase that runs the shipped alert-to-runbook pairing gate over the framework's own deployment sample,
samples/deployment, instead of a fixture. It is the one place in this repository the base runs against real infrastructure. - Depends on - ObservabilityConventionTestsBase (
public sealed class SampleDeploymentObservabilityTests : ObservabilityConventionTestsBase,SampleDeploymentObservabilityTests.cs:12) and the sample's own IaC pair, embedded under the logical namessamples.deployment.main.bicepandsamples.deployment.OPERATIONS.md(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:26-:30, sourced fromMMCA.Common/samples/deployment/main.bicepand.../OPERATIONS.md). - Concept - cross-references the assembly-boundary concept introduced by ObservabilityConventionTestsBaseTests: both are subclasses of the same base that re-point its resource names, but where that class proves the resolution mechanism against a minimal fixture, this class proves the mechanism's real target. The sample is the artifact consumers copy (
samples/deployment/DEPLOYMENT.md, perMMCA.Common/CLAUDE.md), so a runbook section that went missing there would propagate into every repository that lifted it; this is the gate proving its own worked example (SampleDeploymentObservabilityTests.cs:5-:10). [Rubric §13 - Observability & Operability] is the category directly exercised, the same alert-to-runbook pairing rule as its sibling; [Rubric §33 - Developer Experience] applies because the sample is a teaching artifact, and a silently stale one would mislead every adopter rather than just this repository. - Walkthrough - four property overrides. Two re-point the base at the sample's real files:
BicepResource => "samples.deployment.main.bicep"(:14) andRunbookResource => "samples.deployment.OPERATIONS.md"(:16), replacing the base defaultsinfra.main.bicepandinfra.OPERATIONS.md.MinimumAlertSpecs => 4(:20) raises the base's default of 3, because the sample provisions four alerts: failed-requests, server-response-time, availability and ai-token-spend (:18-:19).RequireWorkbook => true(:24) opts into the base's monitoring-view check, which is off by default (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:58). No[Fact]s of its own; inheritance supplies the base's four facts, run against the real sample bicep and runbook instead of a fixture: the alert-to-runbook pairing and severity facts, plusMonitoringWorkbookOrDashboard_IsProvisioned_WhenRequired(ObservabilityConventionTestsBase.cs:67-:77), which returns early unlessRequireWorkbookis on and here asserts the sample bicep declares aMicrosoft.Insights/workbooksorMicrosoft.Portal/dashboardsresource. - Why it's built this way - the class doc states the comment inline (
:18-:19): raisingMinimumAlertSpecsis meant to track the sample provisioning a new alert, never to paper over one going missing, since lowering the count to make a red run go green would defeat the gate's purpose. TheRequireWorkbookopt-in carries its own reason (:22-:23): because consumers copy the sample, it must also ship the operator-facing view the alerts point at (rubric section 13). The base keeps the check off by default so a consumer that does not opt in keeps exactly the alert-to-runbook checks it had (ObservabilityConventionTestsBase.cs:53-:57).sample-deployment-validateinci.yml(MMCA.Common/CLAUDE.md) is the surrounding CI job that keeps the sample itself deployable; this class is the one that keeps its alerts and runbook in lockstep. - Where it's used - an independent class in the Common architecture suite; it has no consumers outside CI, since it is the terminal check on the framework's own sample rather than a fixture other tests build on.
RightModel
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CycleFixtures.Right·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs:6· Level 1 · class
- What it is - the other arm of the deliberate namespace cycle: a type in the
Rightfixture namespace that derives from a base inLeft, supplying theRight -> Leftedge. - Depends on - LeftModelBase (
public sealed class RightModel : LeftModelBase,RightFixtures.cs:6, with theusingof theLeftnamespace at:1). - Concept - cross-references the namespace-cycle concept introduced by LeftModelBase. This half contributes the inheritance edge; LeftService contributes the property-type edge in the other direction. Two edges pointing opposite ways between two namespace nodes is the minimum strongly connected component the rule can report.
- Walkthrough - one auto-property,
string Nameinitialized tostring.Empty(:9), present only so the type is not empty. - Where it's used - the property type of
LeftService.Model, which is what closes the loop; both are scanned by NamespaceCycleFitnessTests.
RoleGatedPage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:48· Level 1 · class
- What it is - an empty public fixture page nested in the route-authorization guard, routed at
/fixture/role-gatedand decorated[Authorize(Roles = AdminRole)](RouteAuthorizationTestsBaseTests.cs:46-:48). It is deliberately unsealed so a subclass can inherit its role gate. - Depends on -
AuthorizeAttributeandRouteAttribute; theAdminRoleconstant of the enclosing test class. - Concept - attribute inheritance is part of the role check.
RequiresRole(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:86) must see a role declared on a base class; this page is that base, and BareOverRoleGatedPage derives from it. - Walkthrough - no members. The theory at
RouteAuthorizationTestsBaseTests.cs:19assertsRoleProbe.Requires(typeof(RoleGatedPage), AdminRole)is true. - Where it's used - only inside its defining file, as a
[InlineData]case and as the base ofBareOverRoleGatedPage.
RoleProbe
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:76· Level 1 · class
- What it is - a private sealed subclass of
RouteAuthorizationTestsBasewhose only job is to expose the protected staticRequiresRoleto the test as a publicRequires(Type page, string role)(:76-:85). - Depends on -
RouteAuthorizationTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs): it supplies the abstractRequiredRole(:31) andIsGovernedPage(:40) members this probe must override. - Concept - a probe subclass satisfies the abstract contract trivially so a protected helper can be tested directly.
TargetAssemblyis this assembly,RequiredRoleisAdminRole, andIsGovernedPagereturnsfalse, so none of the base's inherited facts govern any page here. - Walkthrough -
Requiresforwards toRequiresRole(page, role)and nothing else; the theory feeds it the fixture pages and compares the result to the expected boolean (:27). - Where it's used - only inside its defining file.
RoleThenPolicyPage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:58· Level 1 · class
- What it is - a sealed fixture page routed at
/fixture/role-then-policythat stacks two attributes:[Authorize(Roles = AdminRole)]first, then[Authorize(Policy = "Fixture")](:55-:58). - Depends on -
AuthorizeAttribute,RouteAttribute. - Concept - cross-references the attribute-stacking idea of the sibling
PolicyThenRolePage(:50-:53), which reverses the order. Together the two prove the role check finds the role regardless of which[Authorize]appears first. - Walkthrough - no members; the theory expects
RequiresRoleto be true for it (:21). - Where it's used - only inside its defining file: the theory case at
:21and a governed-page match at:95.
UntaggedTemplate
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:50· Level 1 · class
- What it is - a private, sealed negative-control subclass nested inside CostTagConventionTestsBaseTests. It points the cost-tag gate at a fixture bicep in which a container app carries no
servicetag, so the inherited fact must fail. - Depends on -
CostTagConventionTestsBaseand its three configuration points: the abstractMinimumContainerApps(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36) andDatabaseNamePrefix(:42), and the virtualBicepResourcedefaulting toinfra.main.bicep(:45). - Concept - a negative control proves the gate can fail. The positive case in the outer class runs against
fixtures.costtag-main.bicepand passes; this subclass swaps infixtures.costtag-untagged.bicepwhile keeping every other knob identical (MinimumContainerApps => 2,DatabaseNamePrefix => "Fixture_",CostTagConventionTestsBaseTests.cs:50-:57), so the resource name is the only variable. - Walkthrough - the outer test
UntaggedContainerApp_IsFlaggedByNametakesEveryContainerApp_CarriesAServiceTag(CostTagConventionTestsBase.cs:59) from a newUntaggedTemplateas a delegate and asserts it throws with a message containinguiApp, so the failure must name the app whose spend is unattributed. - Where it's used - only inside its defining file.
WrongPrefixTemplate
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:68· Level 1 · class
- What it is - a private, sealed negative-control subclass nested inside CostTagConventionTestsBaseTests. It reads the well-formed
fixtures.costtag-main.bicepbut declaresDatabaseNamePrefix => "Other_"instead of the fixture'sFixture_(CostTagConventionTestsBaseTests.cs:68-:75). - Depends on -
CostTagConventionTestsBase; the prefix is consumed byServiceDatabases_CarryTheServiceTagOfTheOwningApp, which builds the expected tag texttags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) })from it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:83-:88). - Concept - cross-references the negative-control idea introduced by UntaggedTemplate. Here the template text is correct and the subclass's configuration is wrong, which proves the gate derives the database tag from the repo's own prefix rather than matching any prefix.
- Walkthrough - the outer test
DatabaseTag_UnderAnotherPrefix_IsFlaggedtakesServiceDatabases_CarryTheServiceTagOfTheOwningAppfrom a newWrongPrefixTemplateas a delegate and asserts it throws. - Where it's used - only inside its defining file.
ObservabilityConventionTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14· Level 1 · class
- What it is - a cross-assembly regression guard for the shared SLO alert-to-runbook pairing rule. It subclasses ObservabilityConventionTestsBase from an assembly other than the one the base ships in, re-points it at a fixture IaC pair embedded in this test project, and asserts the base resolves its manifest resources from the derived type's assembly.
- Depends on - ObservabilityConventionTestsBase (
public sealed class ObservabilityConventionTestsBaseTests : ObservabilityConventionTestsBase,ObservabilityConventionTestsBaseTests.cs:14) and the two fixture resources embedded by the csproj under the logical namesfixtures.observability-main.bicepandfixtures.observability-OPERATIONS.md(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:17-:22). Externals:System.Reflection.Assembly, xUnit[Fact], AwesomeAssertions. - Concept introduced - proving a shipped base class's defaults from outside its own assembly. The rule library ships as the
MMCA.Common.Testing.Architecturepackage (ArchitectureRules plus its*TestsBasefamily), so a base can carry a default that is only ever wrong for a consumer:ResourceAssembly => GetType().Assembly(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:51) must bind to the subclass's assembly, because the base's own assembly never contains a consumer's bicep. Testing that default inside the base's own assembly would prove nothing (both assemblies would be the same one), so the guard is deliberately located here, one assembly away. [Rubric §13 - Observability & Operability] assesses whether alerts stay operable, and the rule this class exercises is the one that keeps every provisioned SLO alert paired with a severity-correct runbook section. [Rubric §14 - Testability] applies because this is a meta-test that keeps a shared gate honest, and [Rubric §33 - Developer Experience] because the alternative failure mode is a break that surfaces only in the first downstream repo to adopt the base. - Walkthrough - two property overrides re-point the base's resource names at the fixtures:
BicepResource => "fixtures.observability-main.bicep"(:16) andRunbookResource => "fixtures.observability-OPERATIONS.md"(:18), replacing the base defaultsinfra.main.bicepandinfra.OPERATIONS.md(ObservabilityConventionTestsBase.cs:42,:45). One own[Fact],ResourceAssembly_DefaultsToTheDerivedTypesAssembly(:24-:29), asserts the resolvedResourceAssemblyis the same object as this class's assembly and is not the base's assembly. A second member, the[Theory]HeadingNamesAlert_MatchesTheKeyAsAWholeToken(:38-:61, 21[InlineData]rows at:39-:59), pins the predicate both pairing facts read headings through,HeadingNamesAlert(ObservabilityConventionTestsBase.cs:168): an alert key matches a runbook heading only as a whole token,-alert-{key}then an optional version suffix-v{digits}then end of line, whitespace,(or a closing backtick (regex at:175). The rows cover the accepted shapes (a provisioned-v2or-v12suffix, a backticked<prefix>-alert-...heading, a trailing\r, no space before(sev) and the rejected ones, where a longer hyphenated sibling must not answer for a shorter key (server-response-time-p99versusserver-response-time,x-extraversusx) and a malformed suffix (-v,-v2x,-v2-extra) is a different alert; this is the 062 SLO-alerting-as-code pairing contract (ADR-062). Inheritance supplies the rest: the base's three[Fact]s (ObservabilityConventionTestsBase.cs:81,:63,:91) run against the fixture pair, so the whole discovery-and-pairing path is exercised across the assembly boundary. The fixture bicep declares exactly three well-formed specs plus the two parse anchors the base looks for,var sloAlertSpecsandresource sloAlerts(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-main.bicep:6-:25), which meets the base's defaultMinimumAlertSpecs => 3(ObservabilityConventionTestsBase.cs:39); the fixture runbook carries a matching### fixture-alert-<key> (sev N)heading per spec (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Fixtures/observability-OPERATIONS.md:9-:19), so both pairing directions and the severity check pass. - Why it's built this way - the class doc records the exact regression it exists for (
:5-:13): resolving against the base's own assembly instead of the subclass's is a silent break, since the framework's CI would stay green and only the first adopting consumer would fail. Keeping the guard in a different assembly is the only way to make that difference observable. It is the packaging discipline ADR-058 describes for shipped conformance suites, applied to a fitness base; the rule it guards implements the alerting side of ADR-041. - Where it's used - an independent class in the Common architecture suite. The real adopters are ObservabilityConventionTests in MMCA.ADC and MMCA.Store (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), which subclass the same base with no overrides at all and rely entirely on the default this class pins. - Caveats / not-in-source - the fixture bicep is deliberately minimal (its own header comment says so,
observability-main.bicep:1-:4): it proves the base's parse-and-pair logic, not that any real consumer template is well formed. The consumers' own subclasses do that against their realinfra/files.
OtherRolePlusBarePage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:71· Level 1 · class
- What it is - a nested
public sealedpage fixture carrying[Route("/fixture/other-role")],[Authorize(Roles = "Other")]and a bare[Authorize](RouteAuthorizationTestsBaseTests.cs:68-:71). It is the negative case for the role check: the role gate names a role other thanAdmin, and the bare attribute adds nothing. - Depends on -
Microsoft.AspNetCore.Authorization.AuthorizeAttributeandRouteAuthorizationTestsBase.RequiresRole(exposed by theRoleProbesubclass,:76-:85). - Concept - stacked
[Authorize]attributes are all enforced, so the role check must read every one. A page keeps its gate when any attribute names the required role (class doc,:7-:13). This page proves the converse: no attribute namesAdmin, soRequiresRoleis false (:24) and a governed page like it must still fail. - Walkthrough - no members.
RequiresRole_ReadsEveryAuthorizeAttributeexpectsfalsefor it (:24,:26-:27);MissingRoleTestsgoverns only this page (:105), andGovernedPages_WithoutTheRole_StillFailasserts the failure message contains its name (:37-:44). - Where it's used - only inside its defining file.
PolicyThenRolePage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:53· Level 1 · class
- What it is - a nested
public sealedpage fixture carrying[Route("/fixture/policy-then-role")], a policy-only[Authorize(Policy = "Fixture")]and then[Authorize(Roles = "Admin")](RouteAuthorizationTestsBaseTests.cs:50-:53). - Depends on -
Microsoft.AspNetCore.Authorization.AuthorizeAttributeand theAdminRoleconstant (:16). - Concept - a sibling policy attribute must not hide the role gate.
AuthorizeAttributeallows multiple instances and ASP.NET Core enforces each one, so the role is present even though the first attribute names none. Its mirrorRoleThenPolicyPagereverses the order (:55-:58), so the check is order-independent. - Walkthrough - no members.
RequiresRole_ReadsEveryAuthorizeAttributeexpectstrue(:20);StackedAttributeTestslists it as governed (:93-:96) andGovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGateasserts the gate does not throw (:29-:35). - Where it's used - only inside its defining file.
BareOverRoleGatedPage
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:62· Level 2 · class
- What it is - a sealed fixture page routed at
/fixture/bare-over-role-gatedthat derives from RoleGatedPage and adds a bare[Authorize](:60-:62). - Depends on - RoleGatedPage for the inherited
[Authorize(Roles = AdminRole)]. - Concept - cross-references RoleGatedPage. A bare
[Authorize]on a derived class must not hide the role gate inherited from its base, soRequiresRoleis expected to be true; this is the case that distinguishes a role-aware check from one that only looks at the most-derived attribute. The contrast isBareOnlyPage(:64-:66), which has no role anywhere. - Walkthrough - no members; it is the theory case at
:22and is also matched as a governed page at:96. - Where it's used - only inside its defining file.
CostTagConventionTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs:11· Level 2 · class
- What it is - the cross-assembly guard for the shipped cost-attribution gate. It subclasses
CostTagConventionTestsBase(public sealed class CostTagConventionTestsBaseTests : CostTagConventionTestsBase), supplies a fixture bicep embedded in this test project, and adds four own facts that prove both the happy path and each way the gate must fail. - Depends on -
CostTagConventionTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs) and three nested subclasses: UntaggedTemplate, WrongPrefixTemplate andHighFloorTemplate(MinimumContainerApps => 3against a fixture with two apps). Overrides areMinimumContainerApps => 2,DatabaseNamePrefix => "Fixture_"andBicepResource => "fixtures.costtag-main.bicep". - Concept - cross-references ObservabilityConventionTestsBaseTests, which introduced proving a shipped base's defaults from outside its own assembly. The base's
ResourceAssemblydefaults to the derived type's assembly (CostTagConventionTestsBase.cs:51), so the guard lives one assembly away. This class adds the negative-control triple: each failure mode of the gate has a template that must make it throw. - Walkthrough -
ResourceAssembly_DefaultsToTheDerivedTypesAssemblyasserts the resolved assembly is this class's assembly.UntaggedContainerApp_IsFlaggedByNameexpects the failure to nameuiApp.TooFewContainerApps_FailsTheFloorexpects a throw when fewer apps are discovered than the floor, because a drifted parse anchor would otherwise pass vacuously.DatabaseTag_UnderAnotherPrefix_IsFlaggedexpects a throw when the database tag is derived from another prefix. The inherited factsEveryContainerApp_CarriesAServiceTag(CostTagConventionTestsBase.cs:59) andServiceDatabases_CarryTheServiceTagOfTheOwningApp(:82) also run against the main fixture and must pass. - Where it's used - an independent class in the Common architecture suite (
CostTagConventionTestsBaseTests.cs:11-:76); no consumers outside CI.
InnocentHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:95· Level 2 · class
- What it is - the negative control for the domain-event handler save rule: a handler that mutates in-memory state and returns, which is what a conforming handler looks like.
- Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent (
DomainEventSaveFixtures.cs:95, imports at:1). - Concept - cross-references the handler-save concept from IBadgeGranter. Its contribution is discrimination. The rule walks the call graph out of every method on a handler type up to a depth bound, and a walker that reported any handler with any outbound call would be useless. This fixture makes calls (
GetType(),List<string>.Add,ArgumentNullException.ThrowIfNull) and reaches no save, so a green result for it is a statement that the classifier looked and found nothing rather than that it stopped looking. - Walkthrough - one private
List<string> _seeninitialized with a collection expression (:97), and aHandleAsyncthat guards its argument, records the event's type name, and returnsTask.CompletedTask(:99-:105). No constructor and no injected collaborator, which is the point: nothing it holds can lead to a unit of work. - Why it's built this way - the doc states the ruling directly (
:91-:94): this is what the rule protects, so it must never appear in the report. - Where it's used - asserted absent from the failure message by
HandlerThatOnlyMutates_IsNotFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:58-:67).
InterfaceDispatchSavingHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:81· Level 2 · class
- What it is - the handler fixture whose save is reachable only through an abstraction: it calls IBadgeGranter and never touches a unit of work itself.
- Depends on - IDomainEventHandler<in TDomainEvent>, FixtureDomainEvent, and IBadgeGranter (the constructor-injected collaborator,
DomainEventSaveFixtures.cs:83,:85). - Concept - cross-references IBadgeGranter. The mechanism this fixture forces is
CallGraphIndex.TargetsOf, which the walk calls for every callee that is not itself a save (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:170-:177): an interface method reference is expanded into the implementations present in the scanned modules, and each unvisited, non-allowlisted target is queued one depth deeper. Without that expansion the walk would seeIBadgeGranter.GrantAsyncand stop, because an interface method has no body to read. - Walkthrough - one field, one constructor, and a
HandleAsyncthat returns_granter.GrantAsync(cancellationToken)directly (:87-:88). It is notasync, which is deliberate: the save is exactly two IL calls away with no state machine in between, so the reported chain stays short and the fixture isolates interface resolution from the state-machine traversal that TransitiveSavingHandler exercises. - Where it's used - asserted present in the failure message by
SaveBehindAnInterface_IsResolvedToItsImplementation(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:47-:56), whosebecausestates the ruling: IL records the interface call, so the walk must expand it to the implementations in scope.
LeftService
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CycleFixtures.Left·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs:6· Level 2 · class
- What it is - the
Left -> Righthalf of the fixture cycle: aLefttype whose property type lives in theRightnamespace. - Depends on - RightModel (the nullable
Modelproperty,LeftFixtures.cs:9, with theusingat:1). - Concept - cross-references LeftModelBase. What this type adds is the second edge kind: a property type rather than a base type. The cycle rule records both (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:25-:27), and having one of each in the fixture means a regression that drops either edge kind from the graph builder is caught instead of being masked by the other. - Walkthrough - one member,
public RightModel? Model { get; set; }(:9). Nullability is irrelevant to the rule (it reads the property type); the reference itself is the fixture. - Where it's used - referenced by AcyclicConsumer and scanned by NamespaceCycleFitnessTests.
SharedCodeErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:52· Level 2 · class
- What it is - a fixture that reuses a generic framework static,
Error.NotFoundError("Error.NotFound", ...)(:54), which the allowed-shared-codes list exempts from both the uniqueness and prefix rules. - Depends on -
Error(MMCA.Common.Shared.Abstractions,:1,:54). - Concept introduced - an allowlist is not the same guard as absence of a rule. TicketErrors and DuplicateTicketErrors prove the uniqueness rule catches a real collision; this fixture proves it can still stay silent about a code that is deliberately shared across the codebase (a generic "not found" convenience) once that code is named on the
SharedCodesallowlist passed intoArchitectureRules.ErrorCodesAreUnique. - Walkthrough - one member,
Missing() => Error.NotFoundError("Error.NotFound", "Generic shared code")(:54). - Where it's used -
SharedCode_OnTheAllowList_IsExemptFromUniquenessasserts its name absent from the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:44-:53);PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRuleasserts"Error.NotFound"absent from the prefix-rule failure message (:67-:76);DistinctCodeCount_CountsTheLiteralCatalogcounts it among the five literal codes the catalog must contain (:89-:97).
TicketErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:11· Level 2 · class
- What it is - the well-formed baseline for the error-catalog fitness rule: two properly prefixed codes,
Tickets.NotFoundandTickets.AlreadyClosed. - Depends on -
Error(NotFoundError,Conflict,:13,:15). - Concept introduced - the control every other fixture in the file is measured against: DuplicateTicketErrors collides with one of its codes from a second type, TwoBranchTicketErrors reuses a code inside a single type without colliding, and UnprefixedErrors breaks the prefix convention this type follows correctly.
- Walkthrough - two members,
NotFound()(:13) andAlreadyClosed()(:15), each a one-lineErrorfactory call. - Where it's used -
DuplicateCode_AcrossTwoTypes_IsFlaggedasserts both"Tickets.NotFound"andnameof(TicketErrors)appear in the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:22-:31);PrefixedCodes_AndAllowedSharedCodes_PassThePrefixRuleasserts"Tickets.AlreadyClosed"absent from the prefix-rule failure message (:67-:76); its assembly anchorsFixtureModuleMap's single module (typeof(TicketErrors).Assembly,:112).
TwoBranchTicketErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:28· Level 2 · class
- What it is - reuses one code,
Tickets.Invalid, across two branches of the same type. One error with two exits is not a catalog collision, so the uniqueness rule must stay silent about it. - Depends on -
Error.Validation(:32-:33). - Concept introduced - cross-references TicketErrors. Its contribution is discrimination: a uniqueness rule that flagged any code appearing at more than one call site would also flag this fixture, so the rule must key on the owning type, not the code string alone. DuplicateTicketErrors is the positive control for the same code shared across two types; this is the negative control for the same code repeated inside one.
- Walkthrough - one member,
Invalid(bool alternate), a ternary choosing between"Alternate reason"and"Primary reason"while the code string"Tickets.Invalid"is identical on both branches (:30-:33). - Where it's used -
CodeReusedAcrossBranchesOfOneType_IsNotADuplicateasserts"Tickets.Invalid"absent from the uniqueness failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:34-:42).
UnprefixedErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:37· Level 2 · class
- What it is - carries no module prefix at all (
"SomethingBroke"), so the prefix rule must flag it. - Depends on -
Error.Validation(:39). - Concept introduced - the negative control for
ArchitectureRules.ErrorCodesUseAnAllowedPrefix, paired with TicketErrors (the positive control) and SharedCodeErrors (the allowlisted exemption). - Walkthrough - one member,
Broken() => Error.Validation("SomethingBroke", "No module prefix at all")(:39). - Where it's used -
UnprefixedCode_IsFlaggedasserts both"SomethingBroke"andnameof(UnprefixedErrors)appear in the prefix-rule failure message, so the report names both the code and its owning type (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:56-:64).
DuplicateTicketErrors, DynamicErrors, SharedCodeErrors, TicketErrors, TwoBranchTicketErrors, UnprefixedErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures· (see per-type table) · Level 2 · class
Six internal static classes in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs), each holding one or two expression-bodied factory methods that construct an Error. Together they compile the error catalog the two catalog rules have to judge: well-formed codes, a cross-type collision, an unprefixed code, one code reused across two branches of a single type, a shared framework static, and a code built at run time.
- Depends on - Error and its static factories
NotFoundError,ConflictandValidation(ErrorCodeFixtures.cs:1). Nothing else. - Concept introduced - the error code as the module's public vocabulary, read out of IL. A client switches on
Order.NotFoundand a support ticket quotes it, so two modules both shippingItem.Invalidmake that vocabulary ambiguous in a way that only surfaces in production (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:32-:37). Two rules police it.ErrorCodesAreUnique(:62) fails when the same literal code is constructed by more than one type, and the choice of "type" as the unit is deliberate: one error with two exits in the same class is not a collision, which keeps a normal two-branch handler out of the report (:45-:50).ErrorCodesUseAnAllowedPrefix(:102) fails on any literal code whose prefix a caller-supplied delegate rejects, so the convention stays the consumer's rather than the framework's (:92-:98). Both read the code out of the compiled IL: the scan looks for calls to any of nineErrorfactory names plus.ctor(:17-:29) and reads the first argument, and both deliberately skip the record copy constructor through a first-parameter-is-string check soerror with { Source = ... }is not mistaken for a new code (:11-:15). Codes that are not literals cannot be judged statically, and the rules refuse to guess: they are listed as UNVERIFIABLE in the failure message so the reader knows the catalog has a blind spot (:51-:55). [Rubric §9 - API & Contract Design] assesses whether the published failure vocabulary is governed; [Rubric §15 - Best Practices & Code Quality] covers traceability, since a prefixed code alone says where it came from; [Rubric §14 - Testability] covers the fixtures. - Walkthrough - scope is the per-module Domain and Application assemblies only (
:38-:44), which is why the self-test map registers this assembly as a module Application layer rather than a framework one.
| Type | File:Line | What it models |
|---|---|---|
TicketErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:11 |
The clean catalog: Tickets.NotFound via Error.NotFoundError (:13) and Tickets.AlreadyClosed via Error.Conflict (:15). Correctly prefixed, so it must pass the prefix rule, while being one half of the collision below. |
DuplicateTicketErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:19 |
Ships Tickets.NotFound a second time from a different type, with a different message (:21). The collision the uniqueness rule exists to catch, and the failure message must name both owning types. |
TwoBranchTicketErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:28 |
The near-miss: one method with a ternary that constructs Tickets.Invalid on both branches with different reasons (:30-:33). Two construction sites, one owning type, so it is not a collision and the rule must stay silent. |
UnprefixedErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:37 |
Error.Validation("SomethingBroke", ...) (:39), a code with no module prefix at all. The prefix rule must report it and name the owning type. |
DynamicErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:46 |
The blind spot: Error.Validation("Tickets." + entity, ...) (:48). The code argument is a concatenation rather than a literal, so the scan reports the site as UNVERIFIABLE rather than passing or failing it. |
SharedCodeErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:52 |
Error.NotFoundError("Error.NotFound", ...) (:54), reusing one of the generic statics on Error itself. The allowed-shared-codes list exempts it from both rules, which is what those generics exist for. |
- Why they're built this way - the pairs are what make each rule discriminating rather than merely loud.
TicketErrorsandDuplicateTicketErrorsare the collision;TwoBranchTicketErrorsis the shape a naive "count the construction sites" implementation would wrongly flag;SharedCodeErrorsproves the exemption is honored on the prefix rule as well as the uniqueness rule. Keeping every fixtureinternal staticwith expression-bodied members means the IL is minimal and the reported owner name is unambiguous. See ADR-015. - Where they're used - compiled into this assembly and scanned by ErrorCatalogFitnessTests through its FixtureModuleMap, which anchors the module Application layer on
typeof(TicketErrors).Assembly(ErrorCatalogFitnessTests.cs:125).
DuplicateTicketErrors, DynamicErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures· (see per-type table) · Level 2 · class
Two of the compiled Error-construction sites ErrorCatalogFitnessTests judges: one ships a code that collides with another type, the other builds its code at run time so no static scan can read it.
- Depends on -
Error(MMCA.Common.Shared.Abstractions,:1) and its static factories,Error.NotFoundErrorandError.Validation. - Concept - cross-references the IL-scanned catalog concept the fixture-set class doc introduces (
ErrorCodeFixtures.cs:5-:10): the rule reads error codes out of compiled IL rather than source, so every behavior it must judge (a well-formed code, a cross-type collision, same-type branch reuse, a missing prefix, an allowlisted shared code, a runtime-built code) has to exist as a real compiled call site.DuplicateTicketErrorsis the collision case;DynamicErrorsis the blind spot the rule cannot resolve by static analysis and must instead report asUNVERIFIABLErather than silently passing or failing it. - Walkthrough -
DuplicateTicketErrors.NotFound()callsError.NotFoundError("Tickets.NotFound", "The ticket is missing")(:21), the same code stringTicketErrors.NotFound()constructs (:13), so the two types collide onTickets.NotFound.DynamicErrors.For(string entity)builds its code by string concatenation,"Tickets." + entity(:48), so the code argument is not a literal the scan can read. - Where they're used -
DuplicateCode_AcrossTwoTypes_IsFlaggedasserts the uniqueness rule's failure message names bothTicketErrorsandDuplicateTicketErrors(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:21-:31);DynamicCode_IsReportedAsUnverifiableasserts the prefix rule's failure message containsUNVERIFIABLEand namesDynamicErrors(:78-:87).
| Type | File:Line | The case it models |
|---|---|---|
DuplicateTicketErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:19 |
Ships "Tickets.NotFound", the same code TicketErrors constructs, from a second type: the collision the uniqueness rule catches. |
DynamicErrors |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:46 |
Builds its code by concatenation at run time, so the argument is not a literal and the rule reports the site as UNVERIFIABLE instead of guessing. |
FakeDependentModule
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:31· Level 2 · class
- What it is - the "hard" module fixture: a module that declares dependencies, refuses to start without them, and exports a disabled stub. It exercises every member of the module contract that a leaf module leaves at its default.
- Depends on - IModule (
public sealed class FakeDependentModule : IModule,ModuleConformanceTestsBaseTests.cs:31), ApplicationSettings (theRegisterparameter,:39), IFakeExportService and DisabledFakeExportService (the stub pair,:44), plusIServiceCollectionandIConfigurationBuilderfromMicrosoft.Extensions.*(:1-:2). - Concept introduced - the full module contract as a testable surface. IModule has five members, three of which are defaulted (
Dependencies => [],RequiresDependencies => false,RegisterDisabledStubsas an empty body,MMCA.Common/Source/Core/MMCA.Common.Application/Modules/IModule.cs:17,:23,:34), so the entire contract ModuleLoader registers on can be silently wrong without a compile error. This fixture is the one that overrides all three, so the conformance base is exercised against real (not defaulted) values. [Rubric §14 - Testability] covers the fixture role; [Rubric §7 - Microservices Readiness] is what the stub half protects, since a module that can be switched off without breaking the modules that import its contract is a module that can also be extracted (ADR-059). - Walkthrough
Name => "FakeDependent"(:33): the key ModulesSettings entries and other modules' dependency lists match on.Dependencies => ["FakeLeaf", "FakeOther"](:35): two entries, one of which (FakeLeafModule) exists in the fixture set and one of which deliberately does not, so the list is a pure data declaration rather than a resolvable graph.RequiresDependencies => true(:37): the flag that turns a disabled dependency into a startup failure instead of a substituted stub.Register(...)(:39-:41): an empty body. Registration behavior is not what the conformance base asserts, so the fixture spends nothing on it.RegisterDisabledStubs(IServiceCollection services)(:43-:44): a single expression-bodiedservices.AddSingleton<IFakeExportService, DisabledFakeExportService>(), which is the only member with observable behavior and the one FakeDependentModuleConformanceTests inspects.
- Why it's built this way - the doc comment (
:30) names the intent: this is the shape of the two real consumer modules that are not leaves (Store Sales, ADC Notification). Modeling them locally means the shared base is proven against both module shapes inside MMCA.Common's own CI, before any consumer sees it (ADR-015). - Where it's used - the
TModuleof FakeDependentModuleConformanceTests (:60) and of the deliberately-drifted DriftedTests (:131).
FakeLeafModule
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:15· Level 2 · class
- What it is - the minimal module fixture:
Nameplus an emptyRegister, and nothing else. Its whole purpose is to not overrideDependenciesorRequiresDependencies, so the conformance base has to reach the interface's default implementations to read them. - Depends on - IModule (
public sealed class FakeLeafModule : IModule,ModuleConformanceTestsBaseTests.cs:15) and ApplicationSettings (theRegisterparameter,:19). - Concept introduced - default interface implementations are only reachable through the interface. A default member declared on
IModule(IModule.cs:17,:23) does not exist on the concrete class, somodule.Dependencieswill not compile againstFakeLeafModuleand a reflection lookup on the concrete type finds nothing. The shared base solves this by locating theIModuleinterface by full name and reading the property off the interface (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:80-:99), which dispatches to the override when there is one and to the framework default when there is not. Before the base existed, the hand-written consumer tests needed an explicit(IModule)cast to get the same reach (class doc,:9-:14). [Rubric §14 - Testability] and [Rubric §1 - SOLID] both apply: the fixture exists so the base's interface-dispatch contract is provable rather than assumed. - Walkthrough -
Name => "FakeLeaf"(:17) and an emptyRegister(...)(:19-:21). That is the entire type; the absence of members is the fixture. - Why it's built this way - this is the exact shape the three byte-identical consumer
{X}ModuleTestsfiles collapse into (class doc,:12-:13), so the leaf path is the most-travelled one and the one whose silent breakage would be widest. - Where it's used - the
TModuleof FakeLeafModuleConformanceTests (:51), which in turn is driven directly by two of ModuleConformanceTestsBaseTests's facts (:112-:129).
FixtureSpanishTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Localization·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:37· Level 2 · class
- What it is - a private sealed nested subclass of SpanishAccentTestsBase whose only job is to point the shipped Spanish accent gate at a folder chosen per test, so the enclosing SpanishAccentFitnessTests can run the base's fact over a compliant and an offending fixture tree.
- Depends on - SpanishAccentTestsBase (the base,
SpanishAccentFitnessTests.cs:37; its abstractResourceRootisMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:46). Nothing else. - Concept - cross-references SpanishAccentTestsBase. Its contribution is making the gate testable from both sides: the base's fact
Spanish_resources_keep_their_accents(base:68) scans every*.es.resxunderResourceRoot(base:74), and a reusable gate is only trustworthy if a fixture proves it both passes clean input and fails a real mistake. The class doc names the case it pins (SpanishAccentFitnessTests.cs:6-:11): unaccentedaun("even") is correct Spanish and must be accepted, while a word whose unaccented spelling is the error must still be rejected. [Rubric §14 - Testability] covers the technique. - Walkthrough - a primary constructor taking
string resourceRootand one override,protected override string ResourceRoot => resourceRoot;(:37-:40). No fields beyond the captured parameter. - Why it's built this way - it is
private, so xUnit does not discover it and run the inherited fact against a meaningless root (class doc,:9-:10); the enclosing tests construct it directly and invoke the fact as a delegate. Taking the root as a constructor argument lets one subclass serve both fixture folders instead of one subclass per folder. - Where it's used -
UnaccentedAun_MeaningEven_IsAcceptedconstructs it overSpanishFixtures/Compliantand asserts the fact does not throw (:21-:27);AWordMissingItsAccent_IsStillRejectedconstructs it overSpanishFixtures/Offendingand asserts anXunitExceptionwhose message containstodavia(:29-:35). The fixture root is resolved from the repo root viaArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx")(:14-:19). No usage sites outside the defining file.
MessageBusBackpressureTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Deployment·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs:14· Level 2 · class
- What it is - the self-test for the shipped broker-backpressure gate: it subclasses MessageBusBackpressureTestsBase with one fixture service name (
:16) and drives the base's public staticCheckover inline JSON documents, so every violation message the gate can produce is proven to fire. - Depends on - MessageBusBackpressureTestsBase (the base,
:14; itsCheckisMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:93) and MessageBusSettings (the settings type whoseSectionNameand property names the first fact pins,:21-:23), plus externals xUnit and AwesomeAssertions. - Concept introduced - a gate that parses JSON instead of binding the settings type must be pinned to that type from the side that can see both. The base reads
appsettings.jsonwithSystem.Text.Jsonbecause the testing package takes no framework reference (base class doc,MessageBusBackpressureTestsBase.cs:24-:28), so its key names (MessageBus,PrefetchCount,ConcurrentMessageLimit, base:34-:40) are plain strings. This test class is the pin:KeyNames_MatchTheSettingsTypeTheHostBindsasserts each constant equalsMessageBusSettings.SectionNameornameof(...)of the property (:19-:24), so a rename in Common fails here before a consumer's gate silently stops matching. [Rubric §12 - Resilience & Reliability] assesses bounded consumers (the base'sbecausetext cites it, base:82); [Rubric §14 - Testability] covers the technique of testing a reusable gate over fixture documents. - Walkthrough -
Servicesis overridden to["Fixture"](:16). Each fact callsCheck("Svc", <raw JSON>)and asserts on the returned list:KeyNames_MatchTheSettingsTypeTheHostBinds(:19-:24): the constant-to-settings pin above.BoundedSettings_HaveNoViolations(:27-:29):PrefetchCount16 withConcurrentMessageLimit8 returns an empty list.MissingSection_IsAViolation(:32-:34): a document with noMessageBussection yields the single message "Svc declares no MessageBus section".MissingPrefetch_IsAViolation(:37-:39) andZeroConcurrency_IsAViolation(:42-:44): an absent or non-positive knob yields one violation naming that key (base:109-:117).PrefetchBelowConcurrency_IsAViolation(:47-:49): prefetch 4 against concurrency 8 yields "prefetches 4 but processes 8" (base:119-:124).EmptyServiceList_FailsRatherThanCheckingNothing(:52-:57): a private nestedNoServicessubclass (:59-:62) returns an emptyServices; invoking the base fact on it must throw, which proves the non-vacuity guard (base:72-:73).
- Why it's built this way - the base's real fact reads embedded resources from the consuming project, so it cannot be fed a bad document. Making
Checkpublic (base doc,:86-:89) lets this class exercise the parse directly; the empty-list case reuses the same trick as the other*BaseTestsin this part, a nested subclass configured to be wrong. - Where it's used - an independent class in the Common architecture suite; no usage sites outside the defining file.
MissingRoleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:99· Level 2 · class
- What it is - the negative control for the role check: a private nested subclass of RouteAuthorizationTestsBase that governs a single page, OtherRolePlusBarePage, which carries
[Authorize(Roles = "Other")]plus a bare[Authorize]and never namesAdmin. - Depends on - RouteAuthorizationTestsBase (the base,
RouteAuthorizationTestsBaseTests.cs:99) and the sibling fixture pageOtherRolePlusBarePage(:68-:71), plus theAdminRoleconstant (:16). - Concept - cross-references StackedAttributeTests, its positive twin.
StackedAttributeTestsproves a role gate survives when ANY of a page's authorize attributes names the role; this type proves the loosened check has not become vacuous, because a page whose attributes name a different role must still fail. Without it, a regression that made the check accept any[Authorize]would leave the stacked-attribute cases green. - Walkthrough - three overrides (
:99-:106):TargetAssemblyis the test assembly (:101),RequiredRoleisAdminRole(:103), andIsGovernedPagematches onlytypeof(OtherRolePlusBarePage)(:105). - Why it's built this way - it is
private sealed, so xUnit does not collect its inherited facts as tests of their own (class doc,:12); the enclosing test grabs the inheritedGovernedPages_RequireDeclaredRoleas a delegate instead of running it. - Where it's used -
GovernedPages_WithoutTheRole_StillFailconstructs it and asserts the delegate throws with a message containingnameof(OtherRolePlusBarePage)(RouteAuthorizationTestsBaseTests.cs:37-:44). No usage sites outside the defining file.
NoDomainEventDispatcher
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:128· Level 2 · class
- What it is - a no-op IDomainEventDispatcher stub:
DispatchAsyncreturnsTask.CompletedTaskand touches nothing (:128-:132). - Depends on - IDomainEventDispatcher (
:128) and IDomainEvent (theIEnumerable<IDomainEvent>parameter,:130). - Concept introduced - building a real
DbContextmodel without a real dispatcher.DeleteBehaviorConventionTestsassembles a service provider (:90-:97) purely soDomainEventSaveChangesInterceptorcan be constructed and EF's convention pipeline driven over the model; nothing in a delete-behavior assertion ever raises an event, so the fixture is deliberately inert rather than a mock that records calls it will never receive. - Walkthrough - one member,
DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default) => Task.CompletedTask(:130-:131). No fields, no constructor. - Why it's built this way - the doc comment states the ruling directly (
:127): "Never invoked: no model build dispatches an event." Two sibling stubs beside it in the same file carry the identical rationale for the outbox signal and the entity data-source registry. - Where it's used - constructs the
IServiceCollectionbehind the fixture's service-provider helper alongside the sibling stubs, wired intonew DomainEventSaveChangesInterceptor(new NoDomainEventDispatcher(), ...)(DeleteBehaviorConventionTests.cs:92-:95). Not referenced outside the defining file.
PasswordHashingFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PasswordHashingFitnessTests.cs:15· Level 2 · class
- What it is - a structural credential-storage gate asserted against compiled IL rather than source text: PasswordHasher must depend on
Rfc2898DeriveBytes(a deliberately slow key-derivation function) and onCryptographicOperations(the constant-time comparison), so a rewrite cannot quietly swap either out. - Depends on - PasswordHasher as the assembly anchor and the type under scan (
PasswordHashingFitnessTests.cs:1,:21), ArchitectureAssert (:37,:50), NetArchTest'sTypes/PredicateListquery API (:56-:59), and externals xUnit plus AwesomeAssertions. - Concept introduced - asserting on a dependency edge instead of on an output value. A hashing routine can be verified two ways. A known-answer test pins the values (iteration count, salt length, digest length) and is what
PasswordHasherSecurityTestsdoes; this class pins the shape, which catches a class of change the value tests cannot. Swap PBKDF2 for a single SHA-512 pass and keep the same 32-byte output length and the known-answer pin can be regenerated by whoever made the change, but theRfc2898DeriveBytesreference disappears from the IL and this test goes red. Same for replacingCryptographicOperations.FixedTimeEqualswithSequenceEqual: identical behavior on every test input, and a timing side channel in production. The twobecausestrings spell out the attacks precisely: commodity GPUs trying billions of candidates per second against a stolen table (:38-:39), and a short-circuiting comparison leaking the matching prefix length so a digest is recovered byte by byte (:51-:53). [Rubric §11 - Security] assesses whether credential storage resists offline cracking and side channels; this is the build-time half of that guarantee. [Rubric §14 - Testability] covers the technique, and [Rubric §32 - Dependency & Supply-Chain] applies at the edges, since the assertion is that a specific BCL cryptographic primitive is actually reached. - Walkthrough - two
private const stringfully-qualified type names,CryptographicOperationsType(:17) andRfc2898DeriveBytesType(:19), keep the matched names in one place.Infrastructure(:21) anchors the scanned assembly throughtypeof(PasswordHasher).Assembly, and the privatePasswordHasherTypes()helper (:56-:59) narrows it withTypes.InAssembly(Infrastructure).That().HaveName(nameof(PasswordHasher)).ScannedPasswordHasherSet_IsNotEmpty(:23-:27): the non-vacuity guard, and the first fact for a reason.HaveNamematches on the simple name, so a renamed or moved type would leave the predicate list empty and both dependency assertions would pass having inspected nothing.ContainSingleon the full name (:25-:26) closes that.PasswordHasher_DependsOnASlowKeyDerivationFunction(:29-:40) andPasswordHasher_DependsOnConstantTimeComparison(:42-:54): each runs.Should().HaveDependencyOnAll(<type name>).GetResult()and routes the result throughArchitectureAssert.NoViolations.HaveDependencyOnAllis the positive form (the dependency must be present), which is the unusual direction for an architecture rule and the right one here.
- Why it's built this way - ADR-032 fixes the hashing scheme; the real implementation calls
Rfc2898DeriveBytes.Pbkdf2(MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Auth/PasswordHasher.cs:35) andCryptographicOperations.FixedTimeEquals(:58). Pinning the decision as a fitness function rather than a code-review convention is the ADR-015 approach, and it is the one that survives a refactor by someone who has not read the ADR. - Where it's used - an independent class in the Common architecture suite. It is the shape half of a pair; the parameter values are pinned by
PasswordHasherSecurityTestsin the Infrastructure unit-test project (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Auth/PasswordHasherSecurityTests.cs:21), which holds PBKDF2-HMAC-SHA512 at 600,000 iterations with a 32-byte salt and a 64-byte output as reflected private constants (:20-:22). The class doc here records the division of labour (PasswordHashingFitnessTests.cs:10-:13). - Caveats / not-in-source -
HaveDependencyOnAllreports a reference in the compiled IL, not that the reference is on the hashing path. It cannot tell an actually-used PBKDF2 call from a dead one, which is why the value pins in the companion test remain load-bearing.
ThrottledErrors
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ErrorCodeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ErrorCodeFixtures/ErrorCodeFixtures.cs:61· Level 2 · class
- What it is - an
internal staticfixture that owns one compiledErrorconstruction site built through theError.TooManyRequestsfactory, so the error-catalog rule has a rate-limit code to read out of IL. - Depends on - Error (
internal static Error TooFast() => Error.TooManyRequests("TooFast", "Slow down"),ErrorCodeFixtures.cs:61-:64). Its siblings in the same file, such as UnprefixedErrors, cover the other catalog shapes. - Concept introduced - a factory you do not cover is a hole in the catalog. The rule reads codes from compiled IL (file doc,
ErrorCodeFixtures.cs:6-:7), so everyErrorfactory the scanner recognizes needs a fixture proving its code is picked up. The codeTooFastcarries no module prefix on purpose: ifTooManyRequestswere ignored, the code would escape both the prefix gate and the uniqueness gate unreported. - Walkthrough - one static method,
TooFast()(:63), returning anErrorwith the code"TooFast"and message"Slow down". No state, no branches. - Where it's used - the subject of
TooManyRequestsCode_IsReadIntoTheCatalogin ErrorCatalogFitnessTests (ErrorCatalogFitnessTests.cs:66-:77), which runs ArchitectureRules.ErrorCodesUseAnAllowedPrefixand asserts the thrown message contains"TooFast"andnameof(ThrottledErrors)(:75-:76).
PasswordRuleParityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PasswordRuleParityTests.cs:21· Level 2 · class
- What it is - a client/server validation parity test for the password rule: for every row it asks the Register form's PasswordComplexityAttribute and the server's StrongPasswordRules<T> for a verdict and fails when the two disagree (
PasswordRuleParityTests.cs:40-:55). - Depends on - PasswordComplexityAttribute (
MMCA.Common.UI.Pages.Auth,:3,:45), StrongPasswordRules<T> (MMCA.Common.Application.Validation,:2,:48),System.ComponentModel.DataAnnotationsforValidationContextandValidationResult(:1), and externals xUnit plus AwesomeAssertions. Both sides evaluate the shared PasswordComplexity definition (MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/PasswordComplexity.cs:18), which this test never names directly: it asserts the agreement, not the mechanism. - Concept introduced - a rule implemented twice is tested as a pair, not twice. The form and the API each need the password rule, and the layering forbids them from sharing a validator (UI may depend on Shared only). The fix in source is one definition in Shared that both sides call: the attribute calls
PasswordComplexity.Evaluate(MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Auth/PasswordComplexityAttribute.cs:25) and the server rule chainsPasswordComplexity.MinimumLength,MaximumLengthand the four regexes (MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommonValidationRules.cs:194-:201). This test is what keeps that true: it does not assert either verdict, only that they are equal, so a change that moves one side (a new character class, an ASCII-only regex) goes red even when each side's own unit tests stay green. A disagreement means the form either rejects a password the API would accept, or lets one through that the API then refuses after a round trip (class doc,:8-:12). The rows lean on the Unicode cases because that is where a naive[A-Z]/[^a-zA-Z0-9]implementation diverges:PasswordComplexitytreats any\p{L}(ideographs included) as a letter and any\p{Nd}as a digit (PasswordComplexity.cs:12-:15,:27-:43), so an accented or CJK letter never counts as the special character. [Rubric §24 - Forms, Validation & UX Safety] assesses exactly this client/server parity; [Rubric §14 - Testability] covers the technique. - Walkthrough
ClientPasswordComplexity_MatchesServerStrongPasswordRules(string password, string scenario)(:27-:55): a[Theory]over twelve[InlineData]rows (:28-:39). Seven are ASCII: a valid password, a valid one at exactly the eight-character minimum, then one row each for no uppercase, no lowercase, no digit, no special character, and seven characters. Five are non-ASCII: an accented lowercase letter as the only candidate special character (:35), an uppercase umlaut as the only uppercase (:36), CJK ideographs as the only non-ASCII-alphanumeric characters (:37), CJK beside a full ASCII complement (:38), and a full-width digit as the only digit (:39).- The client verdict is computed the way the EditForm computes it:
GetValidationResultwith aValidationContextwhoseMemberNameisnameof(PasswordProbe.Password), compared againstValidationResult.Success(:44-:47). - The server verdict binds a
StrongPasswordRules<PasswordProbe>top => p.Passwordand readsValidate(probe).IsValid(:48-:50), the same way the API's FluentValidation validators run it over a model. clientVerdict.Should().Be(serverVerdict, ...)(:52-:54) puts the scenario label and the server's verdict in thebecause, so a red row reads as "the Register form and the API must agree on 'full-width digit as the only digit' (server says valid)" rather than a bare boolean mismatch.PasswordProbe(:58) is a private sealed record with onePasswordproperty: the smallest model the generic server rule can be bound to.
- Why it's built this way - the class doc records the placement (
:14-:15): the architecture test project is the one project that references both sides, since the UI test project references MMCA.Common.UI only. The non-ASCII rows are written as\uescapes so the test source stays ASCII (:16-:18). - Where it's used - an independent class in the Common architecture suite; no usage sites outside the defining file. Its subjects are the PasswordComplexityAttribute on the Register form and the StrongPasswordRules<T> the API's validators compose.
- Caveats / not-in-source - parity is asserted only over the twelve rows, and none of them is empty or longer than 128 characters. Those are the two inputs where the sides are not the same rule by design: the attribute returns success on null or empty input and leaves that to
RequiredAttribute(PasswordComplexityAttribute.cs:25), andPasswordComplexity.Evaluatedoes not check the upper bound (PasswordComplexity.cs:46-:57), while the server rule appliesNotEmptyandMaximumLength(PasswordComplexity.MaximumLength)(CommonValidationRules.cs:195,:197).
AcyclicConsumer
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CycleFixtures.Acyclic·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs:6· Level 3 · class
- What it is - the negative control for the namespace-cycle rule: a third fixture namespace that points into
Leftand that nothing points back at, so it must never appear in a cycle report. - Depends on - LeftService (the one-way
Serviceproperty,AcyclicFixtures.cs:9,usingat:1). - Concept - cross-references LeftModelBase. Its role is the discrimination half of the proof: a rule that reported every namespace with any edge would also "pass" the cycle test, so the fixture set needs a namespace that is genuinely coupled yet genuinely acyclic. Depending on
Left(a namespace that is in a cycle) is deliberate: it proves the rule reports strongly connected components, not merely everything reachable from a cycle. - Walkthrough - one member,
public LeftService? Service { get; set; }(:9). - Where it's used - asserted absent from the failure message by
Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces(NamespaceCycleFitnessTests.cs:23-:25).
MutableFixtureId, ExtraStateFixtureId
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures· (see per-type table) · Level 3 · record struct
- What it is - two of the three offending shapes the
StronglyTypedIdsAreReadonlyRecordStructsfitness rule (ADR-115) is proven against: a record struct missingreadonly, and areadonly record structthat wraps a second value alongside the primitive. - Depends on - IStronglyTypedId<TSelf, TValue>, the interface every wrapper in this file (
StronglyTypedIdFixtures.cs) implements. Both sit alongside two compliant siblings,CompliantFixtureIdandCompliantStringFixtureId, and one non-record sibling,NotARecordFixtureId, all in the same file, none of which the rule flags for the two shapes below. - Concept introduced - why the wrapper shape itself is the contract, not just its interface. The strongly-typed-identifier opt-in (
MMCA.Common/CLAUDE.md, "Strongly typed identifiers") is two lines: areadonly record structplus aFromfactory.MutableFixtureIddropsreadonly(record struct MutableFixtureId(int Value) : IStronglyTypedId<MutableFixtureId, int>,StronglyTypedIdFixtures.cs:53), so its wrapped value could be reassigned after an entity has been keyed on it.ExtraStateFixtureIdcarries a second field,Label(readonly record struct ExtraStateFixtureId(int Value, string Label) : IStronglyTypedId<ExtraStateFixtureId, int>,:63-:64), which makes the EF column mapping ambiguous and the converter's round trip lossy; itsFromfactory only ever fills the primitive and leavesLabelatstring.Empty(:67), so the extra state is dead weight by construction. Neither shape fails to compile: only reflection over the compiled type (record-ness, readonly-ness, declared instance field count) can catch them, which is why they are compiled fixtures rather than inline test doubles. [Rubric §14 - Testability] covers the fixture technique; [Rubric §5 - Domain Model] is what the rule protects, a value object that cannot silently mutate or smuggle untyped state past its wrapper. - Walkthrough -
MutableFixtureId.From(int value) => new(value)(:56) is otherwise identical to the compliant declaration; the only diff is the missingreadonlymodifier on the struct itself.ExtraStateFixtureId.From(int value) => new(value, string.Empty)(:67) is the whole type: a two-line factory that proves the second constructor parameter is reachable but never legitimately populated by the framework's own convention.
| Type | File:Line | Notes (what differs) |
|---|---|---|
MutableFixtureId |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:53 |
record struct, not readonly record struct: the missing modifier is the offense. |
ExtraStateFixtureId |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:63 |
readonly record struct but wraps a second field, Label, beyond the primitive Value. |
- Where it's used - both are the subject of one
[Fact]each on StronglyTypedIdFitnessTests:Rule_FlagsAWrapperThatIsNotReadonlyasserts the thrown message containsnameof(MutableFixtureId)and the word "readonly" (StronglyTypedIdFitnessTests.cs:23-:31), andRule_FlagsAWrapperCarryingExtraStateasserts it containsnameof(ExtraStateFixtureId)and "Label" (:33-:41).
EmptyScanTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:180· Level 3 · class
- What it is - the adversarial fixture for the anonymous-endpoint gate's non-vacuity guard: a subclass pointed at an assembly that contains no controllers and no routable components at all, so the scan finds nothing and the guard must fail.
- Depends on - AnonymousEndpointTestsBase (
private sealed class EmptyScanTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:180) and theMMCA.Common.Sharedassembly reached throughtypeof(Shared.Abstractions.Result).Assembly(:121). - Concept introduced - guarding the guard: why a fitness function needs a floor. The allow-list assertion is
offenders.Should().BeEmpty()(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:86-:62), and an empty scan produces zero offenders, so the gate passes loudest exactly when it has stopped looking. A renamed assembly, a moved anchor type, or a repo re-layout all produce that failure mode, and none of them is visible in a green run. The base therefore ships a third fact,ScannedEndpointSet_IsNotEmpty(:66-:76), that counts the discovered controller and routable-component types and requires at leastMinimumScannedTypes, whose default is 1 (:51). This fixture is what proves the counter is real: point it at an assembly that genuinely has neither shape and the fact must throw. Compare the equivalent floors elsewhere in the suite:MinimumScannedTypes => 21on AnonymousEndpointTests,MinimumBaseResources => 3on LocalizationResourceTests,MinimumRoutes = 8in NavigationContractTests, and theDistinctErrorCodeCountandHandledCommandCountinventories the catalog and validator rules expose for the same purpose. [Rubric §14 - Testability] assesses whether the guardrails are themselves trustworthy, and a vacuity floor is the cheapest thing that keeps one honest over a decade of refactors. - Walkthrough - two overrides and an inline comment that states why the chosen assembly works: the Shared package has neither controllers nor routable components (
:119).TargetAssembliesreturns that one assembly (:120-:121), andAllowedAnonymousEndpointsis empty (:123), which is irrelevant here because nothing is discovered to compare against. Beingprivateand nested is what keeps xUnit from collecting its three inherited facts as deliberately-red tests of their own (class doc,:10-:11). - Where it's used - the subject of
Base_Fails_WhenNothingWasScanned(AnonymousEndpointTestsBaseTests.cs:37-:43), which converts the inheritedScannedEndpointSet_IsNotEmptyinto a delegate and asserts it throws (:40-:42). - Caveats / not-in-source - that fact asserts only that an exception is thrown, not what its message says. It proves the floor bites; it does not pin the wording.
CompliantStringFixtureId
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:24· Level 3 · record struct
- What it is - the same positive control as CompliantFixtureId, over a
stringprimitive instead ofint. - Depends on - IStronglyTypedId<TSelf, TValue> closed over itself and
string(public readonly record struct CompliantStringFixtureId(string Value) : IStronglyTypedId<CompliantStringFixtureId, string>,StronglyTypedIdFixtures.cs:24-:25). - Concept introduced - cross-references CompliantFixtureId. Its contribution is discrimination across the primitive type: the fitness rule inspects the shape of the wrapper, not its backing type, and a rule that only ever saw an
int-backed compliant declaration could not prove it treats astring-backed one the same way. - Walkthrough - the primary constructor parameter
Value(:24) and one static factory,From(string value) => new(value)(:27). - Where it's used - the same
IdentifierFixtureMapscan and the sameRule_SaysNothingAboutTheCanonicalDeclarationassertion as CompliantFixtureId (StronglyTypedIdFitnessTests.cs:50).
ArchiveTicketHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:62· Level 3 · class
- What it is - the handler for
ArchiveTicketCommand, a command that carries data (TicketId) and has a handler but no validation of any kind: the shape the command-validator coverage rule must flag. - Depends on -
ICommandHandler<ArchiveTicketCommand, Result>(MMCA.Common.Application.UseCases.Contracts) andArchiveTicketCommand(the record it handles,:60,:62). - Concept introduced - cross-references CreateTicketHandler. Its contribution is discrimination at the far end of the coverage spectrum: CreateTicketHandler has direct coverage and
UpdateTicketHandlerhas bridge coverage, while this one has neither, so it is what proves the rule actually detects an uncovered command rather than passing everything with a handler. - Walkthrough - one member,
HandleAsync(ArchiveTicketCommand command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success())(:64-:65). No constructor, no validator anywhere in the file forArchiveTicketCommand. - Where it's used -
CommandWithNoValidation_IsFlaggedassertsnameof(ArchiveTicketCommand)present in the coverage failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:23-:31);AllowlistedCommand_IsExemptreuses it as the fixture that must stay flagged once a different command is allowlisted, proving the allowlist narrows to exactly the name it lists (:78-:89).
CompliantFixtureId
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:17· Level 3 · record struct
- What it is - the positive control for the strongly typed ID fitness function (ADR-115): the canonical declaration shape, an
int-backed wrapper, that the rule must say nothing about. - Depends on - IStronglyTypedId<TSelf, TValue> closed over itself and
int(public readonly record struct CompliantFixtureId(int Value) : IStronglyTypedId<CompliantFixtureId, int>,StronglyTypedIdFixtures.cs:17). - Concept introduced - the two-line strongly typed ID is exactly two lines, no more. A readonly record struct with a positional
Valueand a staticFromfactory is the whole contract; nothing else is required and nothing else is permitted before the rule starts flagging (see NotARecordFixtureId, MutableFixtureId, ExtraStateFixtureId for the three ways to fail it). This fixture is the shape those three are measured against: a rule that flagged it too would be unusable, since it is the identical shape every real identifier inSource/would take if one adopted the opt-in. - Walkthrough - the primary constructor parameter
Value(:17) and one static factory,From(int value) => new(value)(:18), documented as "the one member the canonical declaration writes" (:18). - Where it's used - the
IdentifierFixtureMapassembly scan inStronglyTypedIdFitnessTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:53-:58) includes it by assembly membership, andRule_SaysNothingAboutTheCanonicalDeclarationasserts its name is absent from the failure message (:44-:50).
CreateTicketHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:21· Level 3 · class
- What it is - the handler for
CreateTicketCommand, the direct-coverage positive control: an explicitCreateTicketCommandValidator(AbstractValidator<CreateTicketCommand>,:16-:19) validates the command itself. - Depends on -
ICommandHandler<CreateTicketCommand, Result>andCreateTicketCommand(:13,:21). - Concept introduced - cross-references ArchiveTicketHandler. Its contribution is the coverage baseline:
AddValidatorsFromAssemblypicks upCreateTicketCommandValidatorby itsIValidator<CreateTicketCommand>closure with no bridge involved, which is the simplest of the three coverage shapes the rule must recognize (direct, bridge, and none). - Walkthrough - one member,
HandleAsync(CreateTicketCommand command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success())(:23-:24). - Where it's used -
CommandWithItsOwnValidator_IsCoveredassertsnameof(CreateTicketCommand)absent from the coverage failure message (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:34-:42); its assembly anchorsFixtureModuleMap's single module (typeof(CreateTicketCommand).Assembly,:121).
ArchiveTicketHandler, CreateTicketHandler, PurgeTicketsHandler, RebuildTicketIndexHandler, ReopenTicketHandler, UpdateTicketHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CommandValidatorFixtures· (see per-type table) · Level 3 · class
Six byte-identical handlers, one per fixture command, each a single expression-bodied HandleAsync returning Task.FromResult(Result.Success()). They do nothing, and that is the design: their only job is to make their command handled, which is the condition that puts a command into the coverage rule's scope at all.
- Depends on - ICommandHandler<in TCommand, TResult> closed over the fixture command and Result (
CommandValidatorFixtures.cs:2-:3). - Concept introduced - "has a handler" is the scoping predicate, not "is named Command". The rule never matches on a naming convention. It projects every concrete class in the module Application assemblies through the closed generic arguments of the command-handler interface and takes the first type argument (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:96-:102). A record that nobody handles is not a command the repo dispatches, so it is not a command the pipeline can fail to validate, and inventing a naming rule instead would both miss unconventionally-named commands and flag DTOs that merely end inCommand. These six handlers exist so the fixture commands satisfy that predicate honestly rather than by declaration. - Walkthrough - every one has the same body:
public Task<Result> HandleAsync({Command} command, CancellationToken cancellationToken = default) => Task.FromResult(Result.Success());. Note the trailing token with its conventional name, which keeps these fixtures compliant with the framework's own cancellation rule even though nothing here awaits.
| Type | File:Line | Its command |
|---|---|---|
CreateTicketHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:21 |
CreateTicketCommand, the directly-validated case. |
UpdateTicketHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:38 |
UpdateTicketCommand, covered through the request bridge. |
ReopenTicketHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:53 |
ReopenTicketCommand, the half-bridge offender. |
ArchiveTicketHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:62 |
ArchiveTicketCommand, the plain offender. |
PurgeTicketsHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:71 |
PurgeTicketsCommand, the offender the allowlist fact exempts. |
RebuildTicketIndexHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommandValidatorFixtures/CommandValidatorFixtures.cs:80 |
RebuildTicketIndexCommand, the payload-free marker. Handled like the rest, and still skipped, because the skip is decided by the payload test rather than by the handler. |
- Why they're built this way - one handler per command, with no shared abstract base, keeps the discovery pass honest: an abstract or open-generic handler would contribute a type parameter rather than a command and would silently shrink the inventory. The uniform empty body means no handler can accidentally become the reason a fact passes.
- Where they're used - counted by
HandledCommandCount(ArchitectureRules.CommandValidators.cs:80), whichCommandInventory_CountsTheDataCarryingCommandspins at exactly 5, the five data-carrying commands with the payload-free marker excluded (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:100-:108).
FakeDependentModuleConformanceTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:60· Level 3 · class
- What it is - the shared module-conformance base wired up against the "hard" fixture module: the subclass shape a real non-leaf module (Store Sales, ADC Notification) uses, with all three expectations declared and the disabled-stub hook implemented.
- Depends on - ModuleConformanceTestsBase<TModule> (
public sealed class FakeDependentModuleConformanceTests : ModuleConformanceTestsBase<FakeDependentModule>,ModuleConformanceTestsBaseTests.cs:60), FakeDependentModule, IFakeExportService, DisabledFakeExportService, andMicrosoft.Extensions.DependencyInjection'sServiceCollection/ServiceLifetime(:70,:77). - Concept introduced - asserting on a
ServiceCollectioninstead of a built container. The disabled-stub hook is a registration contract, not a resolution one, so the fixture never builds a provider: it constructs a bareServiceCollection(:70), callsRegisterDisabledStubson it (:72), and then inspects the resultingServiceDescriptordirectly (:74). That is what lets one assertion cover all three things that can go wrong (wrong service type, wrong implementation type, wrong lifetime) without any of the module's real dependencies having to exist. [Rubric §7 - Microservices Readiness] is the property under guard: a module that can be switched off while its exported contract stays resolvable is a module that can be extracted. [Rubric §14 - Testability] covers the technique. - Walkthrough - three property overrides declare the expectations:
ExpectedName => "FakeDependent"(:62),ExpectedDependencies => ["FakeOther", "FakeLeaf"](:64), andExpectedRequiresDependencies => true(:66). The dependency list is written in the opposite order to the module's own declaration (:35), which is deliberate: the base compares withBeEquivalentTo(ModuleConformanceTestsBase.cs:51), so order must not matter, and writing it reversed proves it does not.AssertDisabledStubs(FakeDependentModule module)(:68-:78) is the one real override: single descriptor forIFakeExportService(:74-:75), implementation typeDisabledFakeExportService(:76), lifetimeSingleton(:77). - Where it's used - collected and run directly by xUnit (it is public and sealed, so its four inherited
[Fact]s are real tests of the fixture module), and referenced by ModuleConformanceTestsBaseTests as the non-leaf half of the base's coverage.
DriftedTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131(and a same-named sibling inApi/AnonymousEndpointTestsBaseTests.cs:100) · Level 3 · class
- What it is - the name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a
private sealedsubclass of a shipped*TestsBasewhose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file. - Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
- Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited
[Fact]s would run and report as red tests of their own. Declaring the drifted subclassprivateand nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;,ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85,AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
| Type | File:Line | What is deliberately wrong |
|---|---|---|
DriftedTests (in AnonymousEndpointTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163 |
Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController. |
DriftedTests (in ModuleConformanceTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 |
Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent" (:33); ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry ["FakeLeaf", "FakeOther"] (:35), so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true (:37). AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here. |
- Walkthrough - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
- Where it's used - the anonymous-endpoint one drives
Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed(AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.
FixtureCountEvent, FixtureNullableCountEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents· (see per-type table) · Level 3 · record
A twin pair of compiled integration-event fixtures that differ in exactly one thing: whether their single Count member is int or int?. Each one's summary names the other as its twin "differing only in the member's nullability" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:31,:35).
- Depends on - BaseIntegrationEvent (both derive from it directly,
ContractShapeFixtures.cs:33,:37). They are scanned by the frozen-contract gate IntegrationEventContractTestsBase through the self-test's privateFixtureMap(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:254). - Concept introduced - a twin fixture isolates one variable. Two types identical in every other respect mean any difference in their rendered contract line can only come from the one axis that differs, here value-type nullability. An
intretyped toint?changes what a consumer must accept, so a snapshot that printed both asInt32would let a breaking wire change through silently. This is the same isolate-one-variable technique the negative fixtures in this chapter use (see DriftedTests). [Rubric §14 - Testability] assesses whether the code's guarantees are provable by tests; this pair proves the contract renderer does not eraseNullable<T>. - Walkthrough - each is a one-line positional
internal sealed recordwith one member; there is no body.
| Type | File:Line | What differs |
|---|---|---|
FixtureCountEvent |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:33 |
(int Count): the non-nullable member. |
FixtureNullableCountEvent |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:37 |
(int? Count): the nullable member. |
- Why they're built this way - the assertion compares the two rendered member lists against each other rather than against hard-coded strings, so it stays true whatever spelling the renderer chooses for a nullable value type, as long as the two differ.
- Where they're used -
Contract_Differs_WhenTheOnlyChangeIsIntToNullableIntin IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:117-:124), which assertsMembersOf(contract, ".FixtureNullableCountEvent ")is not equal toMembersOf(contract, ".FixtureCountEvent ").
FixtureEventNameMissingEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/EventNameFixtures/IntegrationEvents/EventNameFixtures.cs:12· Level 3 · record
- What it is - the deliberately bad integration event for the event-name rule: it carries no
[EventName], so its outbox rows would store a CLR type name that a rename would orphan, and the rule must name it (summary,EventNameFixtures.cs:6-:10). - Depends on - BaseIntegrationEvent (
internal sealed record FixtureEventNameMissingEvent(string Reference) : BaseIntegrationEvent;,EventNameFixtures.cs:12). Its well-formed twin is FixtureEventNameDeclaredEvent. - Concept introduced - a stable wire identity must be declared, never inferred from the CLR name. The outbox persists the event's name, so an event with no EventName ties stored rows to a type name that a rename or namespace move would orphan. [Rubric §7 - Microservices Readiness] assesses stable contracts between services; [Rubric §14 - Testability] covers the negative/positive fixture pair.
- Walkthrough - one plain
string Referencemember and no body; the absence of the attribute is the whole fixture. - Where it's used -
EventWithoutEventName_IsFlaggedinEventNameConventionTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:16-:24) asserts the failure message ofArchitectureRules.IntegrationEventsDeclareEventNamecontainsnameof(FixtureEventNameMissingEvent)(:22); the test'sFixtureProbeMapalso uses its assembly to scope the scan (:48).
FixtureEventNameDeclaredEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/EventNameFixtures/IntegrationEvents/EventNameFixtures.cs:20· Level 3 · record
- What it is - the well-formed twin of FixtureEventNameMissingEvent: it declares
[EventName("Fixture.EventNameDeclared.v1")](EventNameFixtures.cs:19), so the rule must leave it out of the failure message (summary,:14-:17). - Depends on - BaseIntegrationEvent and EventNameAttribute (
MMCA.Common.Domain.Attributes,:1):internal sealed record FixtureEventNameDeclaredEvent(string Reference) : BaseIntegrationEvent;(:20). - Concept introduced - cross-references a stable wire identity must be declared from FixtureEventNameMissingEvent: this is the control proving the rule is selective rather than flagging every event. [Rubric §14 - Testability] applies.
- Where it's used -
EventWithEventName_IsNotFlaggedinEventNameConventionTests(EventNameConventionTests.cs:27-:35) asserts the rule's message does not containnameof(FixtureEventNameDeclaredEvent)(:33).
FixtureCompliantV1, FixtureCompliantV2, FixtureCompliantV3, FixtureContestedV1, FixtureContestedV2, FixtureContestedV3, FixtureBackwardsV1, FixtureBackwardsV2
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents· (see per-type table) · Level 3 · record
Eight throwaway integration-event contracts in one file (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs), forming three groups: a compliant three-rung version ladder, a pair of rival successors to one contested source, and a backwards pair whose "successor" is older than its source. They are the data the two event-upcaster fitness rules are proven against.
- Depends on - BaseIntegrationEvent (every one of them derives from it,
EventUpcasterFixtures.cs:2). Each is a one-parameter positional record carrying a singlestring Sku. - Concept introduced -
SchemaVersionas the ordering a fixture set has to make visible. BaseIntegrationEvent declarespublic virtual int SchemaVersion => 1(MMCA.Common/Source/Core/MMCA.Common.Domain/DomainEvents/BaseIntegrationEvent.cs:32), so a contract that overrides nothing is version 1 and a successor states its own number (ADR-010). That default is what lets these fixtures be as small as they are: the V1 of each group is a bare record and only the successors carry an override. The rule reads the property without running a constructor, throughRuntimeHelpers.GetUninitializedObject(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:118), which is why a get-only virtual returning a literal is the required shape and why no event needs a parameterless factory just to be inspected. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are versioned and governed rather than edited in place; [Rubric §9 - API & Contract Design] applies because a published event is a wire contract; [Rubric §14 - Testability] covers the fixtures. - Walkthrough - the file's header doc records a subtlety worth internalizing (
:9-:13): the contracts sit in a*.IntegrationEventsnamespace so that the residency rule, which EventScopeFitnessTests exercises over this same assembly through a consumer-shaped map, stays satisfied. A fixture that broke a neighbouring rule to prove its own would be a poor fixture. They also never leave this test assembly, so no shipped event contract churns because of them.
| Type | File:Line | SchemaVersion | Role |
|---|---|---|---|
FixtureCompliantV1 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:15 |
1 (inherited default) | First rung of the compliant ladder, and the source of exactly one upcaster. |
FixtureCompliantV2 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:18 |
2 (:20) |
The middle rung: a target of one upcaster and the source of the next. That dual role is the whole point, since it is a chain rather than a duplicate claim. |
FixtureCompliantV3 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:24 |
3 (:26) |
Terminal contract of the ladder. |
FixtureContestedV1 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:30 |
1 (inherited default) | The offending source: two upcasters both read it, which is what the unique-source rule must report. |
FixtureContestedV2 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:33 |
2 (:35) |
One of the two rival successors. |
FixtureContestedV3 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:39 |
3 (:41) |
The other rival successor. Both targets are legal on their own; the offence is the shared source. |
FixtureBackwardsV1 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:45 |
1 (inherited default) | The older contract of the backwards pair, used as the upcaster's target, which is the offence. |
FixtureBackwardsV2 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:48 |
2 (:50) |
The newer contract, used as the upcaster's source. |
- Why they're built this way - three groups because the two rules have three distinct outcomes to prove between them (clean, duplicate claim, wrong direction), and because a chain has to be distinguishable from a duplicate claim. Nothing but
Skuon any of them: the payload is irrelevant to both rules, and every byte of fixture that is not load-bearing is a byte that can mislead a future reader about what is being tested. See ADR-090. - Where they're used - the type arguments of the five fixture upcasters (FixtureCompliantV1ToV2Upcaster and family), named by
nameofin the assertions of EventUpcasterFitnessTests, and, because they are this assembly's only integration events, the live contract IntegrationEventContractTestsBaseTests mutates.
FixtureHopV1, FixtureHopV2
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents· (see per-type table) · Level 3 · record
The first two rungs of a three-rung "double-hop" ladder (FixtureHopV1, FixtureHopV2, FixtureHopV3) declared in MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs. They exist only to be the type arguments of one offending upcaster class, FixtureDoubleHopUpcaster, which declares two upcaster contracts at once (DoubleHopUpcasterFixtures.cs:29-:30).
- Depends on - BaseIntegrationEvent (both derive from it,
DoubleHopUpcasterFixtures.cs:8,:11). They are consumed as the generic arguments of IEventUpcaster inIEventUpcaster<FixtureHopV1, FixtureHopV2>, IEventUpcaster<FixtureHopV2, FixtureHopV3>(:30). - Concept introduced - one upcaster class, one hop. The class summary spells out why a class implementing two
IEventUpcaster<,>contracts is unsafe (:22-:28): the two default interface implementations conflict, so the class must declare the non-genericSourceType,TargetTypeandUpcastexplicitly, and those can report only one source.SourceTypereturnstypeof(FixtureHopV1)andTargetTypereturnstypeof(FixtureHopV2)(:32,:34), so the registry would index the class as a single V1-to-V2 hop and the V2-to-V3 rung would silently vanish.FixtureHopV2sits in the middle of that: it is the declared target of the first contract and the source of the second, the rung the runtime would lose. [Rubric §6 - CQRS & Event-Driven] assesses governed event versioning (ADR-090); [Rubric §14 - Testability] covers the fixture. - Walkthrough - each is a one-parameter positional
internal sealed recordcarrying onlystring Sku.FixtureHopV1has no body and so takes the inheritedSchemaVersionof 1 from BaseIntegrationEvent (MMCA.Common/Source/Core/MMCA.Common.Domain/DomainEvents/BaseIntegrationEvent.cs:32);FixtureHopV2overrides it to 2.
| Type | File:Line | SchemaVersion | Role |
|---|---|---|---|
FixtureHopV1 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:8 |
1 (inherited default) | First rung: the source the offending class reports through SourceType (:32). |
FixtureHopV2 |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:11 |
2 (:13) |
Second rung: target of the first contract and source of the second, the hop the registry would drop. |
- Why they're built this way - the versions ascend correctly (1, 2, 3), so the schema-version rule has nothing to object to and the only defect left is the two-contracts-in-one-class shape. They live in a separate file from the FixtureCompliantV1 family but in the same
*.IntegrationEventsnamespace, so the residency rule stays satisfied. - Where they're used - nowhere outside the defining file. They reach the tests only through
FixtureDoubleHopUpcaster, whichUniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContractsin EventUpcasterFitnessTests expects in the unique-source rule's message, together with the text "exactly one hop" (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44).
FixtureCleanEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:27· Level 3 · record
- What it is - the control fixture for the integration-event payload-purity rule: a one-parameter positional record whose only member is a primitive
string Sku, proving the rule stays quiet on a contract that is actually clean. - Depends on - BaseIntegrationEvent (
internal sealed record FixtureCleanEvent(string Sku) : BaseIntegrationEvent;,PayloadPurityFixtures.cs:27). Sits beside its offending sibling,FixtureLeakingEvent, which nests a payload typed onAuditableBaseEntity<int>(:12-:15,:20) purely to give the rule something to flag. - Concept introduced - cross-references
SchemaVersionas the ordering a fixture set has to make visible from the FixtureCompliantV1 and family section: both fixture sets sit in the samePayloadPurityFixtures/UpcasterFixtures*.IntegrationEventsnamespace convention so the residency rule stays satisfied while they prove an unrelated rule. [Rubric §9 - API & Contract Design] applies because a published event is a wire contract, and payload purity (no domain entities crossing the wire) is part of that contract; [Rubric §14 - Testability] covers the positive/negative fixture pair. - Where it's used - the clean half of IntegrationEventPayloadPurityTests (2 references) and one more site, proving the rule does not flag a contract carrying only primitives.
FakeLeafModuleConformanceTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51· Level 3 · class
- What it is - the same shared base wired up against the leaf fixture, declaring only
ExpectedNameand leaving every other expectation at the base's default. It is the exact subclass shape the three byte-identical consumer{X}ModuleTestsfiles collapse into. - Depends on - ModuleConformanceTestsBase<TModule> (
public sealed class FakeLeafModuleConformanceTests : ModuleConformanceTestsBase<FakeLeafModule>,ModuleConformanceTestsBaseTests.cs:51) and FakeLeafModule. - Concept - cross-references the default-interface-dispatch concept from FakeLeafModule. The pairing is what makes the proof work: the fixture module declares neither
DependenciesnorRequiresDependencies, and this subclass declares neither expectation, so the base's defaults ([]andfalse,ModuleConformanceTestsBase.cs:29,:35) must line up withIModule's defaults read through the interface. If either side stopped reaching the interface, this pair would go red. - Walkthrough - one member:
ExpectedName => "FakeLeaf"(:53). Everything else is inherited, including the vacuousAssertDisabledStubsdefault, which is exactly what makes the fourth fact a no-op here. - Where it's used - collected by xUnit in its own right, and driven directly (as an object, not as a test class) by two of ModuleConformanceTestsBaseTests's facts (
:112-:129).
FixtureHopV3
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:17· Level 3 · record
- What it is - the third rung of the double-hop upcaster fixture (summary,
DoubleHopUpcasterFixtures.cs:16): an integration event at schema version 3, the target of the secondIEventUpcaster<,>contract that the offendingFixtureDoubleHopUpcasterdeclares (:29-:30). - Depends on - BaseIntegrationEvent (
:17). Its siblings in the same file areFixtureHopV1(:8, default schema version) andFixtureHopV2(:11-:14, version 2);FixtureDoubleHopUpcastermaps V1 to V2 and V2 to V3 through IEventUpcaster. - Concept introduced - a rung that would silently vanish. The upcaster's class doc (
:22-:28) explains the trap: a class declaring twoIEventUpcaster<,>contracts must implement the non-genericSourceType,TargetTypeandUpcastexplicitly, and those can report only ONE source (:32-:34pinFixtureHopV1toFixtureHopV2), so the registry would index the class as a single V1 to V2 hop and the V2 to V3 step toward this record would never run.FixtureHopV3exists so that lost rung is a real, compiled type. [Rubric §14 - Testability] applies. - Walkthrough - a positional
internal sealed record FixtureHopV3(string Sku)whose only body ispublic override int SchemaVersion => 3;(:19);Skuis the single payload member every rung carries forward (:36,:38). - Why it's built this way - a third version is the minimum that makes a two-contract upcaster possible, since each contract needs its own distinct source and target; the version override keeps the ladder monotonic so only the unique-source defect is under test.
- Where it's used - only through
FixtureDoubleHopUpcaster(:30,:38);UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContractsin EventUpcasterFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44) asserts the rule's message names that upcaster and contains "exactly one hop". No site outside the defining file namesFixtureHopV3itself.
FixtureShapedBase
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:10· Level 3 · record
- What it is - an abstract intermediate integration-event base sitting between the framework envelope and a concrete fixture event. Its member is part of every derived event's wire shape, so the frozen contract must pin it (summary,
ContractShapeFixtures.cs:6-:9). - Depends on - BaseIntegrationEvent (
:10). Its one subclass isFixtureShapedEvent(:22), which addsint? CountandIReadOnlyList<string> Tags. - Concept introduced - two inheritance layers, two different owners. A consumer may put shared members on its own base record below
BaseIntegrationEvent; those members travel on the wire like any other and belong in the event's contract. The framework envelope's own members, by contrast, are the framework's contract, not the event's. This fixture is what lets the self-test assert both halves of that line at once. [Rubric §14 - Testability] applies. - Walkthrough -
public string Origin { get; init; } = string.Empty;(:13) is the single inherited member; beingabstract, the record is never itself an event in the contract. - Why it's built this way - a renderer that read only
DeclaredOnlyproperties of the concrete event would dropOrigin; one that walked the whole chain would leak the envelope. The expected line,FixtureShapedEvent { Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> }, rules out both. - Where it's used - only through
FixtureShapedEvent:Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBasein IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:86-:98) asserts that line, and that noContractShapeFixturesline containsSchemaVersion:orDateOccurred:(:94-:97). No site outside the defining file namesFixtureShapedBaseitself.
FixtureTallyEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:29· Level 3 · record
- What it is - an integration-event fixture whose one member is a two-argument generic,
IReadOnlyDictionary<string, int> Counts(ContractShapeFixtures.cs:29), so its contract line carries a comma inside angle brackets that the snapshot parser must not split on (summary,:24-:27). - Depends on - BaseIntegrationEvent and
IReadOnlyDictionary<TKey, TValue>(BCL). Scanned by IntegrationEventContractTestsBase. - Concept introduced - a member-list parser must respect generic nesting. The snapshot line separates members with commas, and a naive split would cut
IReadOnlyDictionary<String, Int32>into two bogus members, so a retyped argument would surface as a confusing add-and-remove rather than one changed member. Cross-references the twin-fixture technique at FixtureCountEvent, FixtureNullableCountEvent; [Rubric §14 - Testability] applies. - Walkthrough - a one-line positional
internal sealed recordwith no body. - Where it's used -
Base_Fails_WhenAGenericArgumentOfAMultiArgumentMemberChangesin IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:101-:114): it replaces<String, Int32>with<String, Int64>on theFixtureTallyEventline of a copy of the live contract and asserts the gate throws naming "member Counts changed type".
FixtureTwoContractHandlerBase
MMCA.Common.Architecture.Tests.SliceFixtures ·
MMCA.Common.Architecture.Tests.SliceFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:12· Level 3 · class
- What it is - an abstract query-handler base that serves TWO query contracts, both declared one namespace away in
SliceFixtures.Stranded(summary,FixtureTwoContractHandlerBase.cs:7-:11): the multi-contract negative fixture for slice cohesion. - Depends on -
IQueryHandler<GetFixtureFarQuery, Result>andIQueryHandler<GetFixtureSecondFarQuery, Result>(MMCA.Common.Application.UseCases.Contracts,:13), withResultfromMMCA.Common.Shared.Abstractions(:3). The two contracts areinternal sealed record GetFixtureFarQuery(int Id)(SliceFixtures/Stranded/GetFixtureFarQuery.cs:7) andinternal sealed record GetFixtureSecondFarQuery(int Id)(SliceFixtures/Stranded/GetFixtureSecondFarQuery.cs:4), reached throughusing MMCA.Common.Architecture.Tests.SliceFixtures.Stranded;(:2). - Concept introduced - every handler interface is a registration, so every contract is checked. Registration wires the class for each
IQueryHandler<,>it implements, soHandlersAreCoLocatedWithTheirContracts(ArchitectureRules.Slices.cs:40) must report both stranded contracts rather than whichever interface reflection happens to list first (:8-:10). It is the inverse layout of StrandedFixtureHandlerBase: there the handler sits inStrandedand the contract outside; here the contracts sit inStrandedand the handler outside. [Rubric §14 - Testability] applies. - Walkthrough - two
HandleAsyncoverloads, one per contract (:15-:16,:18-:19), each returningTask.FromResult(Result.Success()), the same trivial body every slice fixture uses. - Why it's built this way - a rule that inspected only the first handler interface would still flag this class for one contract and pass the self-test; asserting on both contract names is what proves the scan walks every interface.
- Where it's used -
AHandlerServingTwoStrandedContracts_IsFlaggedForBothin SliceCohesionFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:56-:64), which asserts the thrown message contains bothGetFixtureFarQueryandGetFixtureSecondFarQuery. The two contract files name it in their summaries.
RebuildFixtureProjectionHandler
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures ·
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:33· Level 3 · class
- What it is - a compiled command-handler fixture with three injected constructor dependencies, the command-side half of the population
ConstructorDependencyCountTestsBase's handler ceiling scans, paired with GetFixtureProjectionHandler as the query-side half of the same proof. - Depends on -
ICommandHandler<RebuildFixtureProjectionCommand, Result>(MMCA.Common.Application.UseCases.Contracts,ConstructorDependencyFixtures.cs:34) and the siblinginternal sealed record RebuildFixtureProjectionCommand(:30), a payload-free marker chosen so the separate command-validator coverage rule that points a map at this assembly does not have to account for it. - Concept introduced - why the command carries no payload. The file's class doc states this directly (
:12-:14): the command is a marker with nothing for the validator-coverage rule to count, so pointing a map at this assembly still counts the same five data-carrying commands it did before this fixture existed. Every injected collaborator (first,second,third) is read back throughCollaborators(:36), the same unread-parameter guard GetFixtureProjectionHandler uses. [Rubric §14, Testability] is the concern. - Walkthrough -
HandleAsync(RebuildFixtureProjectionCommand command, CancellationToken cancellationToken = default)(:38-:41) returnsTask.FromResult(Result.Success()), the same trivial body every fixture in the file uses. - Why it's built this way - three constructor parameters put it one dependency over a tight ceiling (
MaxHandlerConstructorDependencies => 2,ConstructorDependencyCountTestsBaseTests.cs:153) and exactly on a looser one (=> 3,:116); using anICommandHandlerhere and anIQueryHandleron its sibling proves the ceiling scans both contract shapes, not just one. - Where it's used -
Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:36-:49), which asserts the failure message namesRebuildFixtureProjectionHandler (3 ctor dependencies);Handlers_AtTheCeiling_AreNotFlagged(:51-:58) reuses the same fixture, over the sameFixtureMap(:74-:84), against the looser ceiling and asserts no throw.
StrandedFixtureHandlerBase
MMCA.Common.Architecture.Tests.SliceFixtures.Stranded ·
MMCA.Common.Architecture.Tests.SliceFixtures.Stranded·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/Stranded/StrandedFixtureHandler.cs:11· Level 3 · class
- What it is - the negative fixture for slice cohesion: an abstract query-handler base over the same
GetFixturePreferencesQuerycontract as GetFixturePreferencesHandlerBase, but declared one namespace away, in its ownStrandedsub-folder. - Depends on -
IQueryHandler<GetFixturePreferencesQuery, Result>(MMCA.Common.Application.UseCases.Contracts,StrandedFixtureHandler.cs:11) andGetFixturePreferencesQueryfrom the siblingSliceFixturesnamespace, reached across the namespace boundary the fixture exists to create. - Concept introduced - the violation the rule could not see while it read concrete classes only. The class doc states this directly (
:6-:10): an abstract handler base whose contract lives one namespace away is exactly the driftHandlersAreCoLocatedWithTheirContracts(ArchitectureRules.Slices.cs:40) is meant to catch, and it sits in its own folder because the namespace has to follow the folder (IDE0130). [Rubric §14, Testability] is the concern; compare the negative-fixture technique cross-referenced at DriftedTests. - Walkthrough -
HandleAsync(GetFixturePreferencesQuery query, CancellationToken cancellationToken = default)(:13-:16) returnsTask.FromResult(Result.Success()), the same trivial body as its co-located sibling; only the namespace differs. - Why it's built this way - reusing the same contract type as GetFixturePreferencesHandlerBase isolates the one variable the rule checks (namespace distance), rather than introducing a second, unrelated contract that could confound the result.
- Where it's used -
AnAbstractHandlerStrandedFromItsContract_IsFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:21-:30), which asserts the rule's failure message names it.
StubMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:93· Level 3 · class
- What it is - a hand-built IArchitectureMap implementation, written from scratch rather than derived from ArchitectureMapBase, so a test can declare exactly one forbidden namespace for exactly one layer.
- Depends on - IArchitectureMap, LayerRef and the Layer enum (
ModuleIsolationTestsBaseTests.cs:93-:79). Four constructor parameters carry the whole configuration:fromLayer,forbiddenLayer,forbiddenNamespace, and theassemblyto register. - Concept introduced - when the map base is the wrong tool, and why implementing the interface directly is the honest alternative. Every other map in this chapter subclasses ArchitectureMapBase, which derives every other module's namespaces mechanically from the repo token. That derivation is exactly what makes a real map cheap and exactly what makes it useless here: the isolation rules ask a map for
OtherModuleNamespaces(module, layer), and to test one specific cross-layer pair the answer has to be one namespace for one layer and empty for the rest. The base cannot express that, so this stub implements all eleven interface members itself (:81-:110) and puts the whole test inOtherModuleNamespaces, a one-line conditional (:109-:110). The class doc states the reasoning verbatim (:70-:74). [Rubric §7 - Microservices Readiness] is the property the rules under test defend; [Rubric §1 - SOLID] is why this substitution is even possible, since the rule library consumes only the interface and never the base; [Rubric §14 - Testability] is the technique. - Walkthrough
RepoToken => "MMCA.Fixture"(:81) andModuleNames => ["Beta", "Gamma"](:83): two declared modules, so the isolation rules have a cross product to iterate, even though only one of them registers an assembly.Layers(:85-:86) is a singleLayerReffor module"Beta"at the caller'sfromLayer, pointing at the caller's assembly with a derived root namespace.OfLayerand the five convenience accessors (:88-:99) all project off that one list, so a layer with no entry yields an empty sequence and the corresponding rule is vacuous rather than broken.OtherModuleNamespaces(module, layer)(:109-:110) returns the single forbidden namespace whenlayer == forbiddenLayerand an empty array otherwise. That single line is what isolates the pair under test from every other pair the rules would otherwise check.
- Why it's built this way - the test it serves needs a violation that is real: the registered assembly genuinely does depend on
MMCA.Common.Application(it is the Infrastructure assembly,:25), and the map declares that namespace as another module's Application layer. A hand-written map is the only way to arrange a true dependency edge into a fabricated module boundary without inventing assemblies. See ADR-059 for the module boundary the rules enforce. - Where it's used - constructed twice in ModuleIsolationTestsBaseTests: once as the static
CrossLayerViolationshared by two facts (:21-:25) and once as a clean map pointed at a namespace nothing references (:59-:63).
NotARecordFixtureId
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.StronglyTypedIdFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs:40· Level 3 · struct
- What it is - a negative fixture: a strongly-typed identifier wrapper implemented as a plain
readonly structrather than areadonly record struct, so the strongly-typed-id fitness rule can be proven to reject that shape. - Depends on - IStronglyTypedId<TSelf, TValue>, which it implements over
int(public readonly struct NotARecordFixtureId(int value) : IStronglyTypedId<NotARecordFixtureId, int>,StronglyTypedIdFixtures.cs:40). - Concept introduced - cross-references the negative-fixture technique explained at DriftedTests (a deliberately-wrong subclass or shape used to prove an assertion actually bites). Here the "wrong" axis is the CLR shape itself, not a configuration override: the rule requires
record structso it gets value equality and an immutable, printable wrapper for free, and this fixture is the one type in the suite that withholds exactly that. [Rubric §14 - Testability] applies. - Walkthrough - a primary-constructor
readonly structwith one member,public int Value { get; } = value;(:29), and the required factorypublic static NotARecordFixtureId From(int value) => new(value);(:32). Both satisfyIStronglyTypedId<TSelf, TValue>'s contract; only therecordkeyword is missing. - Why it's built this way - a rule that only checked the interface would pass this fixture, which is precisely the gap
StronglyTypedIdsAreReadonlyRecordStructscloses by additionally reflecting on the CLR type shape. - Where it's used -
Rule_FlagsAWrapperThatIsNotARecord(StronglyTypedIdFitnessTests.cs:13-:21), which asserts the thrown message namesNotARecordFixtureIdand mentions "record".
NoEntityDataSources
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:112· Level 3 · class
- What it is - a private nested stub
IEntityDataSourceRegistrythat answers every lookup with the default SQL Server key and everyTryGetwith false, used to build the minimal, realApplicationDbContextmodel the delete-behavior convention test needs without wiring the framework's actual multi-source registry. - Depends on - IEntityDataSourceRegistry (the interface it implements,
DeleteBehaviorConventionTests.cs:112) and DataSourceKey / DataSource (DataSourceKey.Default(DataSource.SQLServer),:114,:116). Registered as the singleton behind a bareServiceCollectionbuilt by DeleteBehaviorConventionTests's own model-building helper (services.AddSingleton<IEntityDataSourceRegistry>(new NoEntityDataSources());,:96). - Concept introduced - a no-op collaborator has to be honest about doing nothing. The class doc (
:107-:111) explains the shape: the twoGetDataSourceKeyoverloads answer the default key rather than throw, andTryGetDataSourceKeyalways answersfalse, because nothing in this test's model-building path calls the "get" overloads and a throwing fixture would trip this repo's own domain-throw architecture rule. With every entity answeringTryGet = false,CrossDataSourceDegradeConvention(MMCA.Common/CLAUDE.md, "Multi-Database Strategy") treats every relationship as single-source, which is exactly the FK-constraints-intact case the delete-behavior rule is checking. [Rubric §14 - Testability] is the concern: a stub collaborator that only ever answers the one shape the test under proof needs, and nothing more. - Walkthrough -
GetDataSourceKey(Type)andGetDataSourceKey(string)both returnDataSourceKey.Default(DataSource.SQLServer)(:114,:116);TryGetDataSourceKey(string, out DataSourceKey)setskey = defaultand returnsfalse(:118-:122);GetPhysicalSourcesInUse()returns an empty array (:124). - Where it's used - registered by the private
Services()helper (:88-:98) that builds theServiceProviderbehind the model-building test context in DeleteBehaviorConventionTests; no site outside the defining file references it.
GetFixtureEntityHandlerBase<TQuery>
MMCA.Common.Architecture.Tests.SliceFixtures ·
MMCA.Common.Architecture.Tests.SliceFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:33· Level 3 · class
- What it is - an abstract query-handler base parameterized over its own contract type, the shape the framework's own
*HandlerBasetypes use:TQueryis a generic parameter, not a concrete type any folder can hold. - Depends on -
IQueryHandler<TQuery, Result>(MMCA.Common.Application.UseCases.Contracts,SliceFixtures.cs:33) andResult(MMCA.Common.Shared.Abstractions). - Concept introduced - a contract that is a type parameter cannot be co-located, so the rule must exempt it.
HandlersAreCoLocatedWithTheirContracts(MMCA.Common.Testing.Architecture,ArchitectureRules.Slices.cs:40) checks that an abstract handler base sits in the same namespace as the concrete request it serves, but a generic contract has no concrete namespace to compare against, so the rule must skip this shape rather than misfire on it. This fixture proves that exemption holds. [Rubric §14, Testability] is the concern. - Walkthrough -
HandleAsync(TQuery query, CancellationToken cancellationToken = default)(:35-:36) returnsTask.FromResult(Result.Success()), the same trivial body every fixture in this file uses; the type exists for its shape, not its behavior. - Why it's built this way - the class doc states the reasoning directly (
:27-:32): it mirrors the framework's own generic*HandlerBasetypes, so the self-test proves the rule tolerates the exact pattern the framework ships. - Where it's used -
AnAbstractHandlerOverAGenericContract_IsSkipped(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:43-:52), which asserts the rule's failure message never names it.
GetFixturePreferencesHandlerBase
MMCA.Common.Architecture.Tests.SliceFixtures ·
MMCA.Common.Architecture.Tests.SliceFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/SliceFixtures.cs:19· Level 3 · class
- What it is - an abstract query-handler base declared beside its concrete contract,
GetFixturePreferencesQuery(SliceFixtures.cs:13), in the same file and namespace: the co-located caseHandlersAreCoLocatedWithTheirContractsmust pass. - Depends on -
IQueryHandler<GetFixturePreferencesQuery, Result>(MMCA.Common.Application.UseCases.Contracts,SliceFixtures.cs:19) and the siblinginternal sealed record GetFixturePreferencesQuery(int UserId)(:13). - Concept introduced - the rule scans abstract handler bases, not only concrete classes. The class doc on
GetFixturePreferencesQuery(:6-:12) states that every handler in this file is deliberately abstract:HandlersAreCoLocatedWithTheirContractsused to read concrete classes only, and these fixtures are exactly what it started seeing once it was widened to abstract bases too. Being query handlers rather thanICommandHandlers also keeps them invisible to the separate command-validator coverage rule that points a map at this same assembly. [Rubric §14, Testability] applies. - Walkthrough -
HandleAsync(GetFixturePreferencesQuery query, CancellationToken cancellationToken = default)(:21-:24) returnsTask.FromResult(Result.Success()). - Why it's built this way - co-location with the concrete contract is the positive case the rule protects; pairing it in the same file as StrandedFixtureHandlerBase, the same base moved one namespace away, isolates the one variable the rule actually checks.
- Where it's used -
AnAbstractHandlerBesideItsContract_IsNotFlagged(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:32-:41), which asserts the rule's failure message never names it.
GetFixtureProjectionHandler
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures ·
MMCA.Common.Architecture.Tests.ConstructorDependencyFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs:48· Level 3 · class
- What it is - a compiled query-handler fixture with three injected constructor dependencies, the query-side half of the population
ConstructorDependencyCountTestsBase's handler ceiling scans, paired with RebuildFixtureProjectionHandler as the command-side half of the same proof. - Depends on -
IQueryHandler<GetFixtureProjectionQuery, Result>(MMCA.Common.Application.UseCases.Contracts,ConstructorDependencyFixtures.cs:49) and the siblinginternal sealed record GetFixtureProjectionQuery(int ProjectionId)declared right above it (:45), kept in the same file "for slice cohesion" per its own summary (:44). - Concept introduced - why the fixture is top-level rather than nested. The file's class doc states it directly (
:7-:14): the ceiling gate skips nested types, so a fixture nested inside the test class would never be scanned, and every injected collaborator (first,second,third) is read back throughCollaborators(:51) so none is an unread primary-constructor parameter (CS9113). [Rubric §14, Testability] is the concern. - Walkthrough -
HandleAsync(GetFixtureProjectionQuery query, CancellationToken cancellationToken = default)(:53-:56) returnsTask.FromResult(Result.Success()), the same trivial body every fixture in the file uses. - Why it's built this way - three constructor parameters put it one dependency over a tight ceiling (
MaxHandlerConstructorDependencies => 2,ConstructorDependencyCountTestsBaseTests.cs:153) and exactly on a looser one (=> 3,:116), the two-sided proof a single fixture with the wrong count could not give on its own. - Where it's used -
Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:36-:49), which asserts the failure message namesGetFixtureProjectionHandler (3 ctor dependencies);Handlers_AtTheCeiling_AreNotFlagged(:51-:58) reuses the same fixture, over the sameFixtureMap(:74-:84), against the looser ceiling and asserts no throw.
StackedAttributeTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:87· Level 3 · class
- What it is - a private nested subclass of RouteAuthorizationTestsBase that governs exactly three fixture pages, each of which carries
[Authorize(Roles = "Admin")]somewhere among several authorize attributes. It is the positive-case drifted subclass for the stacked and inherited attribute coverage. - Depends on - RouteAuthorizationTestsBase (
private sealed class StackedAttributeTests : RouteAuthorizationTestsBase,RouteAuthorizationTestsBaseTests.cs:87) and the three fixture pages it names: PolicyThenRolePage (:53),RoleThenPolicyPage(:58) and BareOverRoleGatedPage (:62). - Concept introduced - a role gate survives sibling and inherited authorize attributes. The suite's class doc (
:7-:13) states the rule:AuthorizeAttributeallows multiple instances and is inherited, and ASP.NET Core enforces every one, so a page keeps its role gate when ANY of its authorize attributes names the role. A policy-only attribute stacked beside the role, in either order, or a derived page's bare[Authorize]must not hide it. [Rubric §14 - Testability] and [Rubric §11 - Security] apply: a role check that read only the first attribute would report a false gap or, worse, a false pass. - Walkthrough -
TargetAssemblyis the test assembly itself (:89),RequiredRoleisAdminRole(:91), andIsGovernedPagereturns true only for the three pages above (:93-:96), so the base's scan sees exactly the stacked and inherited shapes and no others. - Why it's built this way - it is
private, so xUnit does not collect its inherited facts as tests of their own (:12); the siblingMissingRoleTests(:99) is the negative twin that must still fail. - Where it's used -
GovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGate(:29-:35), which takesnew StackedAttributeTests().GovernedPages_RequireDeclaredRoleas a delegate and asserts it does not throw. No site outside the defining file references it.
FixtureLabelEvent, FixtureNullableLabelEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents· (see per-type table) · Level 3 · record
The reference-type counterpart of FixtureCountEvent, FixtureNullableCountEvent: a twin pair whose only difference is nullable reference annotations, both on a plain member (Label) and on a generic argument (Notes). Each summary names the other as its twin "differing only in nullable annotations" (ContractShapeFixtures.cs:39,:44).
- Depends on - BaseIntegrationEvent (
ContractShapeFixtures.cs:42,:47) andIReadOnlyList<T>(BCL). Scanned by IntegrationEventContractTestsBase. - Concept introduced - nullable reference annotations are not in the CLR type. Unlike
int?, astring?is the same runtime type asstring; the annotation lives in compiler-emitted nullability metadata. A contract renderer that read onlyPropertyTypewould print both twins identically, so this pair proves the renderer reads the annotation too, including on a generic argument (IReadOnlyList<string?>). A member that may now be null breaks a consumer that relied on it never being null, which is why the snapshot must see it. Cross-references the isolate-one-variable technique at FixtureCountEvent, FixtureNullableCountEvent; [Rubric §14 - Testability] applies. - Walkthrough - one-line positional
internal sealed records with two members each and no body.
| Type | File:Line | What differs |
|---|---|---|
FixtureLabelEvent |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:42 |
(string Label, IReadOnlyList<string> Notes); renders as { Label:String, Notes:IReadOnlyList<String> } (IntegrationEventContractTestsBaseTests.cs:134-:135). |
FixtureNullableLabelEvent |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:47 |
(string? Label, IReadOnlyList<string?> Notes); renders as { Label:String?, Notes:IReadOnlyList<String?> } (:131-:132). |
- Why they're built this way - unlike the value-type pair, these are pinned to exact strings, because the
?suffix is the very spelling under test: it is what makes the nullable twin render differently from the non-nullable one. - Where they're used - in IntegrationEventContractTestsBaseTests:
Contract_Differs_WhenTheOnlyChangeIsANullableReferenceAnnotation(IntegrationEventContractTestsBaseTests.cs:127-:137) asserts both rendered member lists;Base_Fails_WhenOnlyAMembersNullabilityChanges(:140-:153) rewrites theFixtureNullableLabelEventline fromLabel:String?toLabel:Stringin a copy of the live contract and asserts the gate throws naming "member Label changed type".
FixtureNamedEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:55· Level 3 · record
- What it is - an integration-event fixture that declares a stable wire identity with
[EventName("Fixture.Named.v1")](ContractShapeFixtures.cs:54), so the frozen contract must key it on that name rather than on its CLR type name (summary,:49-:52). - Depends on - BaseIntegrationEvent and EventNameAttribute (
MMCA.Common.Domain.Attributes,:1). Scanned by IntegrationEventContractTestsBase. - Concept introduced - the wire identity, not the CLR name, is the contract key. The
[EventName]value is what the outbox stores, so a CLR rename or namespace move of an event that declares one must not churn the snapshot. [Rubric §7 - Microservices Readiness] assesses whether services can evolve independently over stable contracts; this fixture proves the contract gate keys on the identity a consumer actually sees. [Rubric §14 - Testability] applies too. - Walkthrough -
internal sealed record FixtureNamedEvent(int Value) : BaseIntegrationEvent;(:55): one plain member, no body. - Where it's used -
Contract_KeysAnEventOnItsEventName_WhenItDeclaresOnein IntegrationEventContractTestsBaseTests (IntegrationEventContractTestsBaseTests.cs:156-:166), which asserts the live contract containsFixture.Named.v1 { Value:Int32 }and that no line containsFixtureNamedEvent.
TestingAspireBoundaryTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/TestingAspireBoundaryTests.cs:21· Level 3 · class
- What it is - a layering fitness test for
MMCA.Common.Testing.Aspire, the AppHost test-infrastructure assembly: five facts asserting it depends on none of Domain, Infrastructure, API, UI, or theMMCA.Common.Aspireservice-defaults namespace. It is the runtime half of the boundary; the compile-time half isEnforceTestingAspireLayerBoundaryinSource/Build/MMCA.Common.LayerEnforcement.targets, and this half catches a forbidden dependency that arrives transitively rather than through a declaredProjectReference(:6-:12). - Depends on - AppHostFixtureBase only as the anchor for
TestingAspire => typeof(AppHostFixtureBase).Assembly(:23); ArchitectureAssert for the shared verdict-reporting helper; NetArchTest'sTypes.InAssembly(...).ShouldNot().HaveDependencyOnAny(...)fluent API. - Concept introduced - reuses the dependency-direction assertion pattern this suite applies throughout its layering tests (
ArchitectureAssert.NoViolationsover a NetArchTest result), applied here to a fifth kind of boundary: a testing package's own layer discipline, not a production module's. The reasons differ per forbidden namespace and are stated inline rather than shared: Domain and Infrastructure are ruled out because AppHost test infra drives services over HTTP and has no business coupling to either (:28-:29,:34-:35); API is reached only transitively throughMMCA.Common.Testing(:40-:41); UI is out because the tests exercise HTTP endpoints, never rendered components (:46-:47); andMMCA.Common.Aspire(service defaults) is excluded so the package's own probe-path constants never accidentally pull in the graph they are meant to mirror (:54-:55). [Rubric §7 - Microservices Readiness] is the property under test, since a testing package that leaks a production-layer dependency is a package that cannot be referenced from a thin AppHost without dragging the whole stack with it; [Rubric §14 - Testability] is the technique. - Walkthrough
TestingAspire(:23): the assembly under test, resolved offAppHostFixtureBaserather thanAssembly.GetExecutingAssembly(), so a rename or move of the fixture base is a compile error rather than a silent scope change.TestingAspire_ShouldNotDependOn_Domain/_Infrastructure/_Api/_Ui(:25-:47): four one-line facts, each calling the sharedAssertNoDependencywith a forbidden namespace and abecausereason.TestingAspire_ShouldNotDependOn_ServiceDefaults(:49-:55): the fifth fact, against"MMCA.Common.Aspire"with no trailing dot. A dotted NetArchTest namespace entry matches nothing, so the bare segment form is the one that coversMMCA.Common.Aspireand every namespace beneath it (:51-:52).AssertNoDependency(forbiddenNamespace, reason)(:57-:65): the shared helper every fact calls, running the NetArchTest scan and routing the result through ArchitectureAssert.
- Why it's built this way - ADR-117 introduces the AppHost integration-test base this suite protects; keeping the test package's own dependency graph thin is what lets it be referenced from an AppHost project without pulling Domain or Infrastructure along for the ride. See ADR-117.
- Where it's used - an independent fitness test in the Common architecture suite; it has no consumers of its own.
ConformantTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:225· Level 4 · class
- What it is - the positive fixture for the anonymous-endpoint gate: a subclass whose allow-list correctly names every anonymous endpoint in the fixture set, in both identifier shapes, so all three inherited facts must pass.
- Depends on - AnonymousEndpointTestsBase (
private sealed class ConformantTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:225) and the three fixture controllers it allow-lists. - Concept introduced - building allow-list entries with
typeof(...).FullNameandnameof(...)rather than string literals. Each of the three entries is interpolated from live metadata (:133-:135):$"{typeof(AnonymousFixtureController).FullName}.{nameof(AnonymousFixtureController.PeekAsync)}"for the method-level shape, the same construction against the abstract base for the inherited action, and a baretypeof(TypeLevelAnonymousFixtureController).FullName!for the type-level shape. Renaming any fixture is then a compile error instead of a silently-drifting test. That discipline is not available to a real consumer's list (the framework's endpoints are strings there, as in AnonymousEndpointTests), which is precisely why the stale-entry fact exists to catch whatnameofwould have caught. [Rubric §15 - Best Practices & Code Quality] and [Rubric §14 - Testability] apply. - Walkthrough -
TargetAssembliesis this test assembly (:128-:129), so the scan sees the nested fixture controllers.AllowedAnonymousEndpoints(:131-:136) holds the three entries described above; note the abstract base is listed at its own name rather than the deriving controller's, which is the ruling InheritingFixtureController pins.MinimumScannedTypesis left at the base's default of 1, which the fixture set clears comfortably. One member is added beyond the base's surface:internal IReadOnlyCollection<string> AnonymousEndpointsForTest() => [.. AnonymousEndpoints()];(:138), which materializes the base'sprotectedenumeration so the enclosing test class can assert on the emitted set directly. The base exposesAnonymousEndpoints()asprotectedfor exactly this kind of extension (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:178-:125). - Where it's used -
Base_Accepts_TypeLevelAndMethodLevelEntries(AnonymousEndpointTestsBaseTests.cs:45-:59) runs its three inherited facts, andBase_DoesNotReport_AnInheritedAttributeOnTheDerivedController(:61-:71) reads itsAnonymousEndpointsForTest()output.
DataSourceBranchingFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataSourceBranchingFitnessTests.cs:17· Level 4 · class
- What it is - a text-scanning fitness test over the framework's own
Source/**/*.csthat fails whenever a line names a concrete database engine (aDataSource.Xvalue or an engine-specific context type) outside a fixed per-file allow-list, plus two facts that keep that allow-list exact and four detector facts that pin the regex itself. - Depends on - ArchitectureAssert for the verdict (
:87,:99), ArchitectureMapBase only for its staticFindRepoRoot("MMCA.Common.slnx")(:122); the rule it protects is the IDataSourceEngine strategy split keyed by DataSource. Externals: a source-generatedRegex([GeneratedRegex],:150-:154),System.IO.Directory/File, xUnit[Fact]/[Theory], and AwesomeAssertions. - Concept introduced - a text-level fitness test for a rule the type system cannot see. The class doc states the gap (
:7-:16): engine-specific behavior is meant to live in oneIDataSourceEnginestrategy per engine (ADR-130), but nothing in the compiler stops a newif (engine == DataSource.Sqlite)orcontext is SqliteDbContextbranch from growing back anywhere else. A NetArchTest dependency rule cannot help either, because the offending code references types the file is allowed to reference; what is wrong is the branch, not the dependency. So the gate reads every source file as text and matches one regex (:151):DataSource\.(CosmosDB|Sqlite|SQLServer|PostgreSQL)for an enum value, oris (not )?\w*(SQLServer|Cosmos|Sqlite|PostgreSQL|Npgsql)\w*DbContextfor a type test against an engine context. The allow-list is keyed at file granularity and is two-sided: a hit outside it fails, and a listed file that no longer produces a hit also fails, so the list shrinks with the code instead of keeping a pass for a file that could regress later (:13-:15). [Rubric §34 - Architecture Governance & Documentation] assesses whether architectural decisions are enforced rather than only written down; this test is ADR-130's executable form. [Rubric §8 - Data Architecture] is the property at stake, since a scattered engine branch is what makes a fifth engine or a changed engine rule a hunt rather than one class. [Rubric §14 - Testability] covers the detector facts that prove the regex itself. - Walkthrough
Remedy(:27): the one-line fix text appended to every failure, "route engine-specific behavior through IDataSourceEngine (ADR-130)".AllowedFiles(:32-:76): an ordinalDictionary<string, string>ofSource/-relative paths (forward slashes) to the reason each may name an engine. Twenty-four entries in eight commented groups: the four engine strategies (:35-:38); the four sealed per-engine contexts, one class per engine under ADR-006 (:42-:45); the fourEntityTypeConfiguration*shim bases, since[UseDataSource(DataSource.X)]is how an entity picks its engine (:48-:51); four settings classes whose default engine is a value rather than a branch (:54-:57);DesignTimeDbContextHelperwith its per-engine design-time entry points (:60);IndexBuilderExtensions, whose engine parameter defaults to SQL Server (:64); DataSourceResolver, which owns the framework-default engine constant (:67); and five files that ask for the framework tables' source viaResolveLogical(DataSource.SQLServer, Default), which the resolver maps onto whatever the host configured (:71-:75).ConcreteEngineBranching_StaysInsideTheEngineStrategies(:78-:88): scans, drops every hit whose file is allow-listed, and reports each remaining one asSource/{file}:{line}: {text}.AllowList_HasNoStaleEntries(:90-:102): collects the set of files that produced any hit and fails on every allow-list key absent from it.Detector_Flags_ConcreteEngineBranching(:104-:111): five[InlineData]lines the regex must match, covering an equality test, acaselabel,is,is notwith a pattern variable, and theNpgsqlspelling of a context name.Detector_Accepts_EngineNeutralCode(:113-:118): three lines it must not match, includingdataSourceEngines.Resolve(key.Engine)(the sanctioned route), aDataSourceproperty declaration, and a type test against the engine-neutralApplicationDbContext.ScanSource()(:120-:148): enumerates*.csunder{repo root}/Sourceexcludingbinandobj, in ordinal order, asserts more than 500 files were found (:131, the non-vacuous floor so a wrong scan root cannot pass having read nothing), then matches line by line and records anEngineHitwith a 1-based line number and the trimmed text.EngineReference(:150-:154): the generated regex,CultureInvariant | ExplicitCapture, with a 1000 ms match timeout.EngineHit(:157) is a private record of file, line and text.
- Why it's built this way - ADR-130 moves engine-specific behavior behind one strategy per engine; this gate is what stops the decision decaying one convenient branch at a time. A file-level allow-list with a reason per entry keeps every exception reviewable in one place, and the stale-entry fact means that removing an engine reference from a listed file forces the entry out in the same change.
- Where it's used - an independent fitness test in the Common architecture suite; it has no consumers and scans only Common's own
Source/tree (no*TestsBaseis shipped for Store or ADC to subclass). - Caveats / not-in-source - the scan is textual, so it does not distinguish code from comments or string literals: a doc comment naming
DataSource.SQLServercounts as a hit, which is why several entries (for exampleIndexBuilderExtensionsandDataSourceResolver) cite "and its doc" or "and doc" as part of the reason.
FailingAnonymizeSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AnonymizationFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:37· Level 4 · class
- What it is - an
IAnonymizablefixture whoseAnonymize()always returnsResult.Failure(Error.Invariant("Fixture.CannotErase", ...))(AnonymizationFixtures.cs:37-:43), so the erasure reports failure on its sample instead of overwriting anything. - Depends on -
IAnonymizableandResult/Error(AnonymizationFixtures.cs:1-:2); it carries oneNicknameproperty initialised to"Ada"and never touched, and no[Pii]marker (the fixtures' header comment,:6-:9, explains why: the personal-data inventory self-test scans this assembly for[Pii]members). - Concept - the reverse-PII rule
PropertiesOverwrittenByAnonymizeCarryPiiruns each entity'sAnonymize()on a sample to see which text properties it overwrites. An erasure that fails cannot be inspected, and silently skipping it would hide a broken erasure path, so the rule must report it.ReverseRule_AnonymizeThatFails_IsReportedproves exactly that: the thrown message containsFailingAnonymizeSubject: Anonymize() did not succeed(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:72-:80). - Where it's used - registered as a sample factory in that single test (
AnonymizationRulesTests.cs:76).
FakeArchitectureMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:127· Level 4 · class
- What it is - a map that declares exactly the module and layer pairs a test hands it, with no relation to any real assembly layout. Its only purpose is to let the map-completeness rule's arithmetic be tested directly.
- Depends on - ArchitectureMapBase (
private sealed class FakeArchitectureMap(params IEnumerable<LayerRef>[] modules) : ArchitectureMapBase,LayerDependencyOverrideTests.cs:127) and LayerRef. - Concept - cross-references the map concept from CommonArchitectureMap. What is different here is that the map is data, not a description of reality: a primary constructor takes any number of
LayerRefsequences andDefineLayers()simply flattens them (:131). EveryLayerRefpoints at this same test assembly with a fabricated root namespace of the formMMCA.Fake.{module}.{layer}(:123-:124), andRepoTokenis"MMCA.Fake"(:129), so nothing about the entries can be confused with the framework's own map. The class doc says why that is legitimate (:14-:16): the point under test is the rule's arithmetic over module names and layers, not any particular assembly, so a fixture map is more honest than borrowing a real one. - Walkthrough - the test class supplies the data through two helpers rather than the map:
Full(module)yields aLayerReffor all five layers, andThin(module)yields only Shared, Application and Api (:117-:121), which is the shape of a module with no aggregate and no persistence.Ref(module, layer)(:123-:124) builds each entry. - Why it's built this way - a real map cannot be made to declare a module that is missing a layer without deleting an assembly, which is not something a test can arrange. Constructing the map from literal layer lists turns "module Notification declares no Domain assembly" into a one-line fixture.
- Where it's used - constructed once per fact in LayerDependencyOverrideTests (
:26,:39,:59,:80,:99,:109).
DriftedTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests· (see per-type table) · Level 4 · class
The name is used twice in this assembly, once per fitness base under proof, and both are the same idea: a private sealed subclass of a shipped *TestsBase whose configuration is deliberately wrong, so the base's assertions can be shown to actually fail on the drift they claim to catch. They are nested in different outer classes, so their full names differ and neither is visible outside its own file.
- Depends on - the base each one drifts from, plus the fixture it points at: ModuleConformanceTestsBase<TModule> with FakeDependentModule for one, AnonymousEndpointTestsBase with the fixture controllers for the other.
- Concept introduced - negative fixtures, and hiding them from test discovery. A fitness base that asserts nothing passes everywhere; the only way to know an assertion bites is to feed it a case that must fail. But a public subclass of an xUnit base is itself collected, so its inherited
[Fact]s would run and report as red tests of their own. Declaring the drifted subclassprivateand nested keeps xUnit from collecting it, while the enclosing class can still instantiate it and invoke the inherited methods directly as delegates (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;,ModuleConformanceTestsBaseTests.cs:91). Both class docs record that reasoning in the same words (ModuleConformanceTestsBaseTests.cs:81-:85,AnonymousEndpointTestsBaseTests.cs:10-:11). [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the fixture shape that earns that trust. Compare NavigatingSpec, the same negative-fixture technique applied to a specification rule, and ProbeTests, which parameterizes the drift instead of hard-coding it.
| Type | File:Line | What is deliberately wrong |
|---|---|---|
DriftedTests (in AnonymousEndpointTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163 |
Points TargetAssemblies at this test assembly (:102-:103), so the scan finds the fixture controllers' [AllowAnonymous] attributes, and then supplies an empty AllowedAnonymousEndpoints (:105). Every discovered endpoint is therefore an offender, and the failure message must name AnonymousFixtureController. |
DriftedTests (in ModuleConformanceTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:131 |
Three overrides, each wrong in a different way against FakeDependentModule's real declarations: ExpectedName => "NotTheDeclaredName" (:133) against the module's "FakeDependent"; ExpectedDependencies => ["FakeLeaf"] (:135) against the module's two-entry list, so one dependency is missing; and ExpectedRequiresDependencies => false (:137) against the module's true. AssertDisabledStubs is deliberately not overridden, so the fourth inherited fact stays vacuous here. |
- Why they're built this way - one wrong value per assertion, and no more. If a single drifted subclass were wrong in three ways at once and the base only ever threw on the first, the other two assertions could rot undetected; the module-side fixture avoids that by being driven three separate times, once per fact.
- Where they're used - the anonymous-endpoint one drives
Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed(AnonymousEndpointTestsBaseTests.cs:15-:24); the module one is instantiated three times by ModuleConformanceTestsBaseTests (:91,:99,:107), once per assertion under proof.
FixtureApplicationMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:70· Level 4 · class
- What it is - a one-entry map, private to
SliceCohesionFitnessTests, that registers this test assembly as the map's single Application layer so the slice-cohesion rule reads its handler fixtures and nothing else. - Depends on - ArchitectureMapBase (
private sealed class FixtureApplicationMap : ArchitectureMapBase,SliceCohesionFitnessTests.cs:70) and LayerRef. - Concept - cross-references the map concept taught at FixtureAssemblyMap: a fixture map's only job is to scope a rule's scan to the deliberately-built fixtures instead of the whole repo.
HandlersAreCoLocatedWithTheirContractsreadsmap.OfLayer(Layer.Application)(SliceCohesionFitnessTests.cs:24,:35,:46), so this map declaresFramework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly)(:64) and nothing else. The three facts it feeds prove the rule's newest behavior: an abstract handler base now counts as a slice member, not just its concrete implementation, so a base stranded away from its contract's folder must be flagged (AnAbstractHandlerStrandedFromItsContract_IsFlagged,:22-:30), a base declared beside its contract must not be (AnAbstractHandlerBesideItsContract_IsNotFlagged,:32-:41), and a base parameterized over a generic contract, which has no folder any type could co-locate with, must be skipped entirely (AnAbstractHandlerOverAGenericContract_IsSkipped,:43-:52).[Rubric §14, Testability]assesses whether the co-location guard is provable with focused inputs;[Rubric §34, Architecture Governance and Documentation]is why the guard exists, since slice cohesion is what keeps a CQRS handler and the contract it serves in one folder a reviewer can see at once. - Walkthrough -
RepoToken => "MMCA.Common"(:60) and a single-entryDefineLayers()returningFramework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly)(:62-:65). - Where it's used - constructed once as a readonly field on
SliceCohesionFitnessTests(:19) and passed toArchitectureRules.HandlersAreCoLocatedWithTheirContractsin all three facts (:24,:35,:46).
FixtureAssemblyMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs· (see per-type table) · Level 4 · class
The name is used seven times in this assembly, once per IL-reading rule under proof, and all seven are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these seven rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through
ScannableAssemblyLocations(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule readsmap.OfLayer(Layer.Domain)and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:111-:111), so registering Domain is mandatory there. All four useFramework(...)rather thanModule(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk byAssembly.Locationalso explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. [Rubric §14 - Testability] assesses whether a guardrail is provable with focused inputs, and a map that scopes the scan to fixtures is what makes each of these seven rules provable at all. - Walkthrough - each declares
RepoToken => "MMCA.Common"and a single-entryDefineLayers()naming one layer and one anchor type from the fixture namespace it serves. Anchoring bytypeof(SomeFixture).Assemblyrather than byAssembly.GetExecutingAssembly()is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
| Type | File:Line | Layer and anchor | The rule it scopes |
|---|---|---|---|
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 |
Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) |
AggregatesCascadeSoftDeleteToChildren. |
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 |
Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) |
HardDeletesOnlyInAllowedTypes. |
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 |
Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) |
DomainEventHandlersDoNotSave. |
FixtureAssemblyMap (in DomainThrowFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 |
Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:156) |
DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing. |
FixtureAssemblyMap (in ClockReadTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:98 |
Framework(Layer.Domain, typeof(InjectedClockFixture).Assembly) (:104) |
DomainAndApplicationDoNotReadTheClock (:88). |
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 |
Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) |
QueryHandlersUseReadRepositories (:48). |
FixtureAssemblyMap (in SubstitutabilityRuleTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:73 |
Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly) (:79) |
SubstitutableMembersDoNotRefuseTheContract (:25), which flags an override or interface implementation that throws NotSupportedException or NotImplementedException. |
- Why they're built this way - seven nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
- Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Most are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (
DomainThrowFitnessTests.cs:29-:33). - Where they're used - constructed once as a readonly field per test class (
CascadeSoftDeleteFitnessTests.cs:21,SoftDeleteEnforcementFitnessTests.cs:19,DomainEventHandlerSaveFitnessTests.cs:21,DomainThrowFitnessTests.cs:36,ClockReadTests.cs:18,QueryHandlerReadRepositoryTests.cs:18,SubstitutabilityRuleTests.cs:20) and passed to every rule call in each.
FitnessPrincipal
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:57· Level 4 · class
- What it is - a throwaway "principal" entity used only as test data for the specification-navigation fitness function. It is the entity that a dependent record points at, so a specification can be written that tries to reach across the navigation into it.
- Depends on - AuditableBaseEntity<TIdentifierType> (it is a
public sealed class FitnessPrincipal : AuditableBaseEntity<int>,SpecificationFitnessTests.cs:57). - Concept introduced - test fixture entities for a fitness function. A fitness function is an executable architecture rule (see ArchitectureRules); to prove such a rule actually fires you feed it a deliberately-crafted model rather than the real domain.
FitnessPrincipalis one half of that crafted model: a bare entity carrying a single scalar (IsActive,SpecificationFitnessTests.cs:59) so a specification can navigate to it. [Rubric §14 - Testability] assesses whether rules are provable with focused inputs; this fixture exists precisely so the guard is tested against a known-unsafe shape instead of hoping a real specification trips it. - Walkthrough - one auto-property,
bool IsActive(:59). That is the only member; identity and audit fields come from the base. It is the navigation target referenced byFitnessDependent.Principal. Deriving fromAuditableBaseEntity<int>is load-bearing rather than cosmetic: the rule's navigation walker only counts a property as an entity navigation when its type (or its collection element type) inherits the auditable entity base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:121-:138). - Why it's built this way - the fitness test must be non-vacuous: it needs a real cross-entity navigation to flag. A minimal principal with a single scalar is the smallest thing a specification can legally navigate into.
- Where it's used - referenced by FitnessDependent and, through it, by NavigatingSpec and NavigatingQuerySpec; the whole fixture set drives SpecificationFitnessTests.
BareMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:133· Level 4 · class
- What it is - a map whose Api and Application layers both point at this test assembly, deliberately chosen because it holds neither a controller nor a CQRS handler. It is the vacuous-scan fixture for
ConstructorDependencyCountTestsBase's two newer populations. - Depends on - ArchitectureMapBase (
private sealed class BareMap : ArchitectureMapBase,:87) andCommon.Shared.Abstractions.Resultpurely as an anchor assembly, not becauseResultitself is scanned. - Concept - cross-references the map concept introduced at FixtureAssemblyMap: a fixture map's job is to scope a rule's scan to a deliberately-built input rather than the whole repo. Where that family proves a rule fires correctly on a real offender,
BareMapproves the opposite failure mode, that a scan finding zero candidates must not pass silently. It declaresModule("Bare", Layer.Api, typeof(Common.Shared.Abstractions.Result).Assembly)and the same forLayer.Application(:92-:93), so both layers resolve to an assembly with no controller and no handler in it. - Walkthrough -
RepoToken => "MMCA.Common"(:89) and a two-entryDefineLayers()(:91-:94), one per scanned layer. - Why it's built this way - reusing
Result's assembly as the anchor, rather than declaring a fresh fixture type, keeps the map to a two-line body: the assembly's contents are irrelevant, only that it contains neither shape the rule looks for. - Where it's used - the
MapofEmptyScanTests(ConstructorDependencyCountTestsBaseTests.cs:180), whichBothPopulations_FailVacuousScans(:60-:72) drives to assert both the controller and the handler ceiling facts throw on a scan that finds nothing to check.
AnonymousEndpointTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:13· Level 4 · class
- What it is - the meta-test for the shipped anonymous-endpoint base: ten facts proving each assertion fails on the drift it claims to catch, that both allow-list identifier shapes are accepted, that an inherited attribute is not double-reported on the derived controller, and (SEC-ADC-03) that the default-on "undecorated endpoint" scan carries the same fail/accept/stale-entry guarantees without ever double-reporting an endpoint the anonymous scan already covers.
- Depends on - AnonymousEndpointTestsBase (the type under test), its own nested fixtures (AnonymousFixtureController and family, InheritingFixtureController,
UndecoratedFixtureController, DriftedTests,StrictDriftedTests, StaleAllowListTests,StaleUndecoratedAllowListTests, EmptyScanTests, ConformantTests,StrictConformantTests), plus xUnit[Fact]and AwesomeAssertions' delegate assertions. - Concept introduced - cross-references the "test a shipped test base from the outside by invoking its facts as delegates" technique introduced by ModuleConformanceTestsBaseTests. The base's original job was one scan: fail on an anonymous endpoint the allow-list missed, and accept both identifier shapes (method-level and type-level
[AllowAnonymous]) the list is written in. SEC-ADC-03 adds a second scan, on by default, for the gap that leaves: a controller that never got[Authorize]at all is invisible to an allow-list keyed on[AllowAnonymous], because it carries no attribute for the scan to find.RequireExplicitAuthorizationDecisiondefaults totrue(AnonymousEndpointTestsBase.cs:69; a repo can override it to opt out), and this suite proves the scan symmetrically to the original: fails when an undecorated controller is unlisted (StrictDriftedTests), does not double-report an endpoint whose decision is inherited from an abstract base, accepts a reviewed undecorated entry, fails on a stale undecorated entry, and, critically, fails by default on a subclass that overrides nothing, so the gate cannot be silently absent. The class doc states the reasoning (:8-:11). [Rubric §14 - Testability] assesses whether guardrails are trustworthy; [Rubric §11 - Security] is the property at stake, since a broken or unadopted undecorated-endpoint gate is exactly the "forgot [Authorize]" class of bug SEC-ADC-03 names; [Rubric §33 - Developer Experience] covers the failure mode, a break that would only appear downstream after a release (ADR-058). - Walkthrough
Base_Fails_WhenAnAnonymousEndpointIsNotAllowListed(:15-:24): drives DriftedTests and asserts the thrown message namesAnonymousFixtureController(:22), with thebecausestating the ruling, that the offender message must name the endpoint that lost its gate (:23).Base_Fails_WhenTheAllowListHasAStaleEntry(:26-:35): drives StaleAllowListTests and asserts the message contains"NoLongerAnonymous"(:33), the type name from an entry that matches nothing.Base_Fails_WhenNothingWasScanned(:37-:43): drives EmptyScanTests and asserts the non-vacuity fact throws (:42).Base_Accepts_TypeLevelAndMethodLevelEntries(:45-:59): the positive case. It calls all three of ConformantTests's inherited facts inside one delegate (:50-:55) and assertsNotThrow(:57-:58). Running all three together matters: the allow-list is only correct if it is simultaneously complete (no offenders) and exact (no stale entries).Base_DoesNotReport_AnInheritedAttributeOnTheDerivedController(:61-:71): reads the scan output through ConformantTests'sAnonymousEndpointsForTest()and asserts the derived-controller identifier is absent (:69-:70). This is the only fact that inspects the emitted set rather than an assertion's outcome.Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll(:73-:84): drivesStrictDriftedTests(RequireExplicitAuthorizationDecision => true, empty allow-list) and asserts the thrown message namesUndecoratedFixtureController(:81), because "a controller that forgot[Authorize]is invisible to the[AllowAnonymous]allow-list" (:82-:83).Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBase(:86-:97): readsStrictConformantTests().UndecoratedEndpointsForTest()and asserts neitherInheritingFixtureControllernor the abstract base itself is reported (:91-:96), the same inherited-attribute discipline the anonymous scan already applies.Base_Accepts_AnUndecoratedEndpointThatIsAllowListed(:99-:105): drivesStrictConformantTests, whoseEndpointsWithoutAuthorizationAttributeis the scan's own output fed back in, and assertsNotThrow.Base_Fails_WhenTheUndecoratedAllowListHasAStaleEntry(:107-:114): drivesStaleUndecoratedAllowListTestsand asserts the message contains"NoLongerUndecorated"(:113).Base_RequiresAnExplicitDecisionByDefault(:118-:125): drives plainDriftedTests, which never overridesRequireExplicitAuthorizationDecision, against the same fixture assembly that containsUndecoratedFixtureController, and asserts the thrown message names it (:124). The gate is on by default (AnonymousEndpointTestsBase.cs:69); the comment (:116-:117) records why the old off-by-default branch was replaced: it asserted two disjoint-by-construction scans did not intersect, which could never fail.
- Why it's built this way - all six drifted and conformant subclasses (including the two SEC-ADC-03 additions,
StrictDriftedTestsandStaleUndecoratedAllowListTests) areprivate, which is what keeps xUnit from collecting their inherited facts as deliberately-failing tests of their own (class doc,:10-:11). The base ships in theMMCA.Common.Testing.Architecturepackage, so proving it here is proving it before a release rather than after one. - Where it's used - an independent class in the Common architecture suite. The base it protects is bound for real by AnonymousEndpointTests here and by the equivalent subclass in each consumer repo.
CascadeChildFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:17· Level 4 · class
- What it is - the child entity every cascade fixture aggregate owns a collection of. It is what makes those collections child-entity collections in the rule's eyes rather than ordinary lists.
- Depends on - AuditableBaseEntity<TIdentifierType> closed over
int(CascadeFixtures.cs:17, import at:1). - Concept introduced - soft-delete does not cascade, and the shape of a rule that can tell. Setting
IsDeleted = trueon an aggregate root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans are invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was "deleted" months ago. The database cannot help either, because a soft delete is an ordinary UPDATE and there is noON DELETE CASCADEto fire (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:35-:42).AggregatesCascadeSoftDeleteToChildren(:100) therefore fails any concrete type deriving fromAuditableAggregateRootEntity<T>that declares a collection ofAuditableBaseEntity<T>children and does not delete them in its ownDelete()override. The child-collection test is deliberately narrow: an instance field on the aggregate whose type is one of six named generic collection types (:25-:33) over an element type deriving from the auditable entity base (:200). Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field, and the backing field name is normalized back to the property name for the report (:192-:197,:242). The aggregate's own_domainEventsescapes twice over: it is declared on the base rather than on the aggregate, and IDomainEvent is not an entity (:66-:68). [Rubric §8 - Data Architecture] assesses whether the data model's lifecycle rules actually hold; [Rubric §30 - Compliance/Privacy/Data Governance] is the sharper consequence, since an orphaned child row is data nobody accounts for at erasure time (ADR-005); [Rubric §14 - Testability] covers the fixtures. - Walkthrough - one member,
public string Label { get; init; } = string.Empty;(:19). Deriving from AuditableBaseEntity<TIdentifierType> is the entire fixture: strip that base and every collection in the file becomes invisible to the rule, which is precisely what ChildlessFixture'sList<string>demonstrates from the other side. - Why it's built this way - the file doc explains why every fixture here is
internaland in its own namespace (:10-:15): it keeps them invisible to the framework rules that run over CommonArchitectureMap, which registers theSource/assemblies and never this test assembly. - Where it's used - the element type of five collections across the six cascade aggregates, and the assembly anchor of CascadeSoftDeleteFitnessTests's FixtureAssemblyMap (
CascadeSoftDeleteFitnessTests.cs:103).
AbstractAnonymizableSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AnonymizationFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:52· Level 4 · class
- What it is - an
internal abstractfixture that implementsIAnonymizablebut leavesAnonymize()abstract. Its summary calls it "an abstract anonymizable base: not a concrete entity, so it needs no sample" (AnonymizationFixtures.cs:51). - Depends on - IAnonymizable (
internal abstract class AbstractAnonymizableSubject : IAnonymizable,:52) andResultfor the abstractpublic abstract Result Anonymize();(:54). - Concept introduced - a coverage rule must skip what cannot be instantiated.
AnonymizableEntitiesAreCoveredBySamples(on ArchitectureRules) demands a sample factory for every concrete anonymizable entity. An abstract base has no instance to sample, so demanding one would misfire. This fixture is the abstract candidate that proves the exemption holds, alongside the concrete UnsampledSubject that must be reported and the non-anonymizable NotAnonymizableSubject that must not. [Rubric §14 - Testability] and [Rubric §30 - Compliance/Privacy/Data Governance] apply. - Walkthrough - one abstract member,
Anonymize()(:54); no state and no body, since only its type shape matters to the rule. - Why it's built this way - keeping it in the candidate list beside the concrete subjects lets one assertion cover both directions: the failure message names the unsampled concrete type and never names the abstract base.
- Where it's used - the candidate arrays of
CoverageRule_AnonymizableEntityWithoutSample_IsReported(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:110, asserted absent from the message at:123) andCoverageRule_EveryConcreteAnonymizableSampled_Passes(:134), where sampling only the two concrete subjects is enough for the rule to pass.
FixtureModuleMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractsandMMCA.Common.Architecture.Tests.Cqrs· (see per-type table) · Level 4 · class
The name is used twice in this assembly, once per catalog-style rule under proof, and both are the same one-entry map: this test assembly registered as a module Application layer so a consumer-facing rule sees the fixtures. They are nested in different outer classes, so their full names differ.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept introduced - why a self-test sometimes has to pretend to be a consumer. Several rules deliberately read only the per-module assemblies and skip framework layers, because a framework's own types are not the consumer's catalog: the error-catalog rules take
ModuleDomain()plusModuleApplication()(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:39-:44), and the validator-coverage rule takesModuleApplication()alone, mirroring where the framework'sAddValidatorsFromAssemblyscan looks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:88-:89). A map built fromFramework(...)entries hasModuleset to the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95) and the module-layer lookup keeps only entries withl.Module.Length > 0(:101-:102), soModuleApplication()returns nothing and both rules would pass having read nothing at all. UsingModule("Tickets", ...)is therefore not decoration: it is the one thing that puts the fixtures in scope. Compare FixtureAssemblyMap, whose rules read whole layers and which usesFramework(...)for the opposite reason, and FakeConsumerMap, which flips the same switch to prove a rule excludes framework types. - Walkthrough - both declare
RepoToken => "MMCA.Common"and a single-entryDefineLayers()returningModule("Tickets", Layer.Application, <anchor>.Assembly). The module name"Tickets"is arbitrary except that the error-catalog self-test's prefix delegate closes over the same word (ErrorCatalogFitnessTests.cs:113), which is how a consumer's real prefix convention is modelled.
| Type | File:Line | The rule it scopes |
|---|---|---|
FixtureModuleMap (in CommandValidatorCoverageFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:115 |
Anchored on typeof(CreateTicketCommand).Assembly (:121). Its doc records the mirroring intent: the rule reads commands and validators from the per-module Application assemblies, exactly where the framework's validator scan looks (:110-:114). |
FixtureModuleMap (in ErrorCatalogFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:119 |
Anchored on typeof(TicketErrors).Assembly (:112). Its doc notes that the catalog rules read the per-module Domain and Application assemblies and deliberately skip framework layers (:102-:105). |
- Why they're built this way - each is nested
private sealedinside the test class that uses it, so neither is visible to the other and neither can be mistaken for a repo map. Duplicating eight lines is cheaper than a shared fixture map whose meaning would have to be read from another file. - Where they're used - constructed once as a readonly field per test class (
CommandValidatorCoverageFitnessTests.cs:20,ErrorCatalogFitnessTests.cs:19) and passed to every rule call in each.
FixtureShapedEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ContractShapeFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ContractShapeFixtures/IntegrationEvents/ContractShapeFixtures.cs:22· Level 4 · record
- What it is - an
internal sealedintegration-event fixture with two constructed-generic members,int? CountandIReadOnlyList<string> Tags, that derives from an intermediate consumer base instead of directly from the framework envelope (ContractShapeFixtures.cs:22). It exists so the frozen wire-contract snapshot can be proven to spell generic arguments out and to include a member inherited from a consumer base. - Depends on - the private fixture base
FixtureShapedBase(ContractShapeFixtures.cs:10), which adds one member,string Origin(:13), and itself derives from BaseIntegrationEvent (:10). - Concept - cross-references the frozen-contract concept taught at IntegrationEventContractTestsBase. The snapshot line for an event lists its public instance properties sorted by name, drops anything declared in the framework envelope namespace
MMCA.Common.Domain.DomainEvents(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:75,:68), and renders each type by its simple name with generic arguments spelled out (:91-:96). This fixture carries the two shapes a naive renderer gets wrong. ANullable<Int32>orIReadOnlyList<String>printed as the open-generic name would hide a retyped argument, and an intermediate base sitting between the event and the envelope is still part of the wire shape, so itsOriginmember must appear while the envelope'sSchemaVersionandDateOccurredmust not.[Rubric §9, API & Contract Design]assesses whether published contracts are versioned and guarded against silent breaks; this fixture is what makes the snapshot's type rendering checkable rather than assumed.[Rubric §14, Testability]covers the self-test it feeds. - Walkthrough - a positional record with no body. Its siblings in the same file each isolate one other rendering rule:
FixtureTallyEventa comma inside angle brackets (ContractShapeFixtures.cs:29),FixtureCountEvent/FixtureNullableCountEventvalue nullability (:33,:37),FixtureLabelEvent/FixtureNullableLabelEventreference nullability (:42,:47), andFixtureNamedEventan[EventName]wire key (:54-:55). - Where it's used - read through the fixture map by IntegrationEventContractTestsBaseTests, whose
Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBaseasserts the live contract contains the exact line...FixtureShapedEvent { Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> }and that noContractShapeFixturesline carriesSchemaVersion:orDateOccurred:(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:85-:98). The live contract comes fromArchitectureRules.BuildIntegrationEventContract(new FixtureMap())(:198-:199). - Caveats / not-in-source - being
internalkeeps it off the test assembly's public surface; the contract builder evidently enumerates non-public types, since the assertion at:90-:93passes against it, but the enumeration helper itself was not read for this section.
ModuleConformanceTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:86· Level 5 · class
- What it is - the meta-test for the shipped per-module conformance base: five facts that prove each of the base's assertions actually fails on the drift it claims to catch, that the base reaches
IModule's default implementations for a leaf module, and that its disabled-stub hook is harmlessly vacuous when a subclass does not override it. - Depends on - ModuleConformanceTestsBase<TModule> (the type under test), its own fixtures DriftedTests, FakeLeafModuleConformanceTests, FakeLeafModule and FakeDependentModule, plus xUnit
[Fact]and AwesomeAssertions'Should().Throw<Exception>()/NotThrow()delegate assertions. - Concept introduced - testing a shipped test base, from the outside, by invoking its facts as delegates. The base lives in the
MMCA.Common.Testing.Architecturepackage and is consumed by five subclasses across MMCA.ADC and MMCA.Store, so a regression in it would surface as green consumer suites that assert nothing. This class converts each inherited[Fact]into a method group (var assert = new DriftedTests().Module_ShouldDeclare_ExpectedName;,:91) and asserts on the delegate's behavior, which is what lets one xUnit test observe another test method failing. [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; [Rubric §34 - Architecture Governance & Documentation] is the reason the guardrail exists at all (ADR-015 makes module conformance build-gating); and [Rubric §33 - Developer Experience] covers the failure mode being prevented, a silent break that would only appear in a downstream repo after a release (ADR-058). - Walkthrough
- Three adversarial facts, one per assertion the base makes, each pointing at DriftedTests and asserting the delegate throws:
Base_Fails_WhenTheNameDrifts(:88-:94),Base_Fails_WhenADependencyIsMissing(:96-:102), andBase_Fails_WhenRequiresDependenciesDrifts(:104-:110). Base_ReadsDefaultInterfaceImplementations_ForALeafModule(:112-:121): instantiates FakeLeafModuleConformanceTests and calls the two inherited assertions directly (:119-:120). Passing proves the base reachedIModule's defaults, because the leaf fixture declares neither member (inline comment,:115-:116).Base_DisabledStubHook_IsVacuous_ByDefault(:123-:129): asserts the fourth inherited fact does not throw for a module that exports nothing, so a leaf subclass never has to overrideAssertDisabledStubs(the base's default is an empty body,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:75-:77).- The nested
private sealed class DriftedTests(:131-:138) closes the file; being private is what keeps xUnit from collecting its three deliberately-failing inherited facts as tests of their own (class doc,:81-:85).
- Three adversarial facts, one per assertion the base makes, each pointing at DriftedTests and asserting the delegate throws:
- Why it's built this way - the three module members the base asserts on are the entire contract ModuleLoader registers on: it topologically orders on
Dependencies, matches ModulesSettings entries byName, and chooses between a hard start failure and stub registration fromRequiresDependencies(the base's ownbecauseclauses spell each one out,ModuleConformanceTestsBase.cs:40,:52,:61). None of those drifts throws at build time, so the fitness base is the only guard, and this class is the guard on the guard (ADR-059). - Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed by five real module tests:
MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5,MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:5,MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5, andMMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5. - Caveats / not-in-source - the three adversarial facts assert only that an exception is thrown (
Should().Throw<Exception>(),:93,:101,:109), not which assertion produced it or what its message says. They prove the base is not vacuous; they do not pin its failure text.
FixtureProbeMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:43· Level 4 · class
- What it is - a
private sealedone-entry architecture map nested inEventNameConventionTests:RepoToken => "MMCA.Common"(EventNameConventionTests.cs:45) and a singleFramework(Layer.Shared, typeof(FixtureEventNameMissingEvent).Assembly)layer (:47-:48), so the event-name rule sees this test assembly's fixture events and nothing else. - Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept - cross-references the scoped-fixture-map pattern taught at FixtureAssemblyMap: register only the layer the rule reads so the assertion is provable against a known shape. This map uses
Framework(...)(notModule(...)) because the[EventName]rule reads the shared layer.[Rubric §14, Testability]covers the focused input;[Rubric §9, API & Contract Design]is why an event without[EventName]matters (its assertion message says a CLR name would be persisted and a rename would orphan it,:22). - Walkthrough - the map is built once per fact in
EventWithoutEventName_IsFlagged(:16) andEventWithEventName_IsNotFlagged(:27). The third fact,FrameworkEvents_AllDeclareEventName(:38), runs against the real framework instead, so the fixture map is not used there. - Where it's used - only inside
EventNameConventionTests; nothing references it outside its own file.
PluralSentenceResourceTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/PluralSentenceResourceTests.cs:15· Level 4 · class
- What it is - a resource-file fitness test: one fact that scans every framework
.resxfor a count rendered inside a fixed plural sentence ("{0} items in your cart"), plus three detector self-tests that pin what the scanner flags and what it accepts. - Depends on - ArchitectureMapBase for
FindRepoRoot("MMCA.Common.slnx")(PluralSentenceResourceTests.cs:20), ArchitectureAssert forNoViolations(:35-:38), and the plural API it steers toward, StringLocalizerPluralExtensions. Externals:System.Xml.Linq.XDocumentfor reading.resxentries (:72-:77), a source-generatedRegex(:111-:112), xUnit and AwesomeAssertions. - Concept introduced - a count inside a single-form sentence is a localization defect, and it is detectable mechanically.
"{0} items"reads wrongly for a count of one, and translators cannot fix it because the key has only one form. The framework's answer is a pair of sibling keys,KEY.OneandKEY.Other, resolved byL.Plural(KEY, count, count), which picks.Onefor exactly one and.Otherotherwise and falls back to the base key when the plural key is missing (MMCA.Common/Source/Presentation/MMCA.Common.UI/Globalization/StringLocalizerPluralExtensions.cs:23,:26,:40-:51; ADR-027). This test turns that convention into a build failure whose message is the instruction: split the key and useL.Plural(PluralSentenceResourceTests.cs:37-:38).[Rubric §27, i18n]assesses whether text adapts correctly to language and quantity; this fixes pluralization at the resource layer, for the English and Spanish sets the framework ships.[Rubric §14, Testability]covers the detector self-tests, and[Rubric §34, Architecture Governance & Documentation]the choice of an executable convention over a written one. - Walkthrough
FrameworkResources_HaveNoCountInAFixedPluralSentence(:17-:39): enumerates*.resxunder the repo'sSourcefolder, skippingMudTranslations*files and anything underbinorobj(:20-:27), then asserts a non-vacuous floor of at least four files (SharedResource and ErrorResources, base pluses) so a wrong scan root cannot pass having read nothing (:29-:30). Every violation is reported asfile: key = "value" (count followed by 'word')(:32-:33,:94).Detector_Flags_ACountInAFixedPluralSentence(:41-:48): four inline cases, including the(s)hedge ("{0} item(s) in your cart"), a Spanish sentence, and a count in parentheses, each expected to produce exactly one violation.Detector_Accepts_AValueThatIsNotAFixedPluralSentence(:50-:58): five cases that must produce none: a.Otherkey, a singular noun after the placeholder, a verb ("{0} with Id {1} was not found."), a preposition, and a.Onekey.Detector_Accepts_TheBaseKeyOfAPluralPair_AsItsFallback(:60-:67): a base key whose.Othersibling exists is skipped, because it is only the fallback for a resource set not yet split.FixedPluralSentences(:85-:95) is the detector: it drops.Otherkeys and any key with a.Othersibling (:89-:90), then matches each value againstCountFollowedByPlural, a placeholder ({0},{1:N0}) followed by whitespace and a word with an optional(s)/(es)suffix (:110-:112, 1000 ms match timeout).IsPluralMarked(:97-:102) counts a word as plural when it ends in(s)or(es), or is four or more letters ending insand not inNonNounsEndingInS, a fixed English and Spanish stop list (does,this,antes,entoncesand so on,:105-:108).
- Why it's built this way - the detector is a heuristic, so it is proven from both sides with inline data rather than trusted: the flag cases pin its recall and the accept cases pin the false positives it must not raise. The resource scan is a plain XML read of
dataelements (:72-:77) rather than aResourceManagerload, so it needs no culture setup and sees every key in every satellite file. - Where it's used - an independent class in the Common architecture suite; nothing references it outside its own file. The framework call site it protects is
NotificationSend, which resolvesL.Plural("Notif.Send.SentTo", ...)(MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Notifications/NotificationSend.razor.cs:137). - Caveats / not-in-source - the four-letter, ends-in-
srule is English-shaped. A plural of three letters or fewer ("{0} ids") or an irregular plural not ending ins("{0} children") passes unflagged, and a non-noun ending insthat is missing from the stop list would be a false positive. The scan covers MMCA.Common's ownSourcetree only; consumer repos are not scanned by this class.
FixtureEntity
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.HardDeleteFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:11· Level 4 · class
- What it is - the entity type the hard-delete fixtures erase, keep, and soft-delete. It is the
Tof theDbSet<T>,IQueryable<T>andList<T>those fixtures operate on. - Depends on - AuditableBaseEntity<TIdentifierType> closed over
int(HardDeleteFixtures.cs:11, import at:2). - Concept introduced - soft-delete as the framework's deletion model, and the EF members that bypass it. An entity sets
IsDeleted = trueand the global query filter hides it, so the row survives for audit, restore and erasure accounting; a hard delete bypasses all of that (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:26-:30).HardDeletesOnlyInAllowedTypes(:62) reads every scannable assembly's IL and reports any call to one of four EF Core member names,Remove,RemoveRange,ExecuteDeleteandExecuteDeleteAsync(:10-:11), from a type outside the repo's allowlist. The interesting half is the disambiguation.Removeis an ordinary collection method name, so it only counts when the declaring type is one of four EF entity-set types,DbContext,DbSet<T>,InternalDbSet<T,T>orLocalView<T>(:18-:24,:138-:139);ExecuteDeleteandExecuteDeleteAsyncare unambiguous wherever EF declares them, so any declaring type under theMicrosoft.EntityFrameworkCoreprefix counts (:130,:138). Without that scoping the rule would fire on every dictionary and cache in the repo, which is the version of this idea that gets deleted after a week. [Rubric §8 - Data Architecture] assesses whether the storage model's deletion semantics hold everywhere; [Rubric §30 - Compliance/Privacy/Data Governance] is why it matters, since an erased row silently drops out of audit and erasure accounting (ADR-005); [Rubric §14 - Testability] covers the fixtures. - Walkthrough - one member,
public string Name { get; set; } = string.Empty;(:13). It ispublic(unlike the fixtures around it) because it appears in the public signatures of the static fixture methods and is the anchor type the self-test's map resolves the assembly from (SoftDeleteEnforcementFitnessTests.cs:83). - Where it's used - the entity parameter of all five members across DbSetRemovingFixture and family, and the map anchor of SoftDeleteEnforcementFitnessTests.
FixtureCompliantV1ToV2Upcaster, FixtureCompliantV2ToV3Upcaster, FixtureContestedClaimUpcaster, FixtureRivalClaimUpcaster, FixtureBackwardsVersionUpcaster
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents· (see per-type table) · Level 4 · class
Five internal sealed upcasters over the eight fixture contracts, each a single expression-bodied Upcast that copies the one field across. Between them they are the truth table for the two event-upcaster fitness rules (ADR-090): two clean rungs of a chain, two rivals claiming one source, and one pointing backwards down the version ladder.
- Depends on - IEventUpcaster in its two-parameter form (
EventUpcasterFixtures.cs:1) and the fixture event contracts they are generic over. - Concept introduced - the upcast chain must be a function, and it must run forwards. When a retired contract arrives from the outbox, the consumer resolves the one upcaster registered for that type and replays the message as its successor. Two properties make that mechanical rather than lucky. First, at most one upcaster may read a given source contract: with two claimants, which one runs would depend on DI registration order, so
EventUpcastersHaveUniqueSourceTypesgroups by source and reports any group with more than one entry (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:14-:17). Second, the target must declare a strictly higherSchemaVersionthan the source, because an upcaster that moves sideways or down is not producing a successor at all and the chain stops being a ladder anyone can reason about;EventUpcastersIncreaseSchemaVersioncompares the two declared versions and flagstargetVersion <= sourceVersion(:44-:48). Note what the second rule deliberately does not do: a missing or non-intSchemaVersionis skipped rather than reported (:37-:42), because that is the business of the separateIntegrationEventsDeclareSchemaVersionrule. One rule, one judgement. Both rules find their subjects by matching the interface on name and arity,"IEventUpcaster`2"(:81-:83), which is how the rule library stays free of a compile dependency on the framework's own Application package. [Rubric §6 - CQRS & Event-Driven] assesses whether asynchronous contracts evolve safely; [Rubric §9 - API & Contract Design] covers the versioning discipline; [Rubric §29 - Resilience & Business Continuity] is the operational consequence, since an outbox row written against a retired contract has to be replayable weeks later. - Walkthrough - every one of the five has the same body shape,
public {Target} Upcast({Source} integrationEvent) => new(integrationEvent.Sku);, which is the typed overload declared by IEventUpcaster (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Events/IEventUpcaster.cs:82);SourceTypeandTargetTypecome free as default interface implementations off the generic arguments (:72,:75), which is exactly what the rules read.
| Type | File:Line | The case it models |
|---|---|---|
FixtureCompliantV1ToV2Upcaster |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:54 |
Clean: the only claimant on FixtureCompliantV1, and its target declares version 2. Must be absent from both rules' reports. |
FixtureCompliantV2ToV3Upcaster |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:60 |
The second rung, and the sharper of the two clean cases: its source is the previous upcaster's target. That is a chain, not a duplicate claim, and a naive implementation that grouped on "types that appear in more than one upcaster" would wrongly flag it. It gets its own dedicated fact. |
FixtureContestedClaimUpcaster |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:66 |
Offender: reads FixtureContestedV1 and produces V2. Legal in isolation. |
FixtureRivalClaimUpcaster |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:72 |
The second claimant on the same FixtureContestedV1, producing V3 instead. The offence is the pair, so the rule's message must name the contested contract and both upcasters. |
FixtureBackwardsVersionUpcaster |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/EventUpcasterFixtures.cs:78 |
Offender for the other rule: source FixtureBackwardsV2 (version 2), target FixtureBackwardsV1 (version 1). It is the only claimant on its source, so it passes the unique-source rule and fails the version rule, which is what keeps the two rules' proofs independent. |
- Why they're built this way - the two rules judge different things about the same set of types, so the fixture set is designed so that each offender trips exactly one of them. If the backwards upcaster also shared a source, a failure in the unique-source rule would mask a regression in the version rule. Being
internalkeeps them off the assembly's public surface while still visible toConcreteClasses, which is what the rule enumerates (ArchitectureRules.Upcasters.cs:86). - Where they're used - reflected over by EventUpcasterFitnessTests through UpcasterTestMap, and named by
nameofin its assertions.
FixtureDoubleHopUpcaster
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.UpcasterFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/UpcasterFixtures/IntegrationEvents/DoubleHopUpcasterFixtures.cs:29· Level 4 · class
- What it is - an
internal sealedoffender fixture: one class that declares two upcaster contracts at once,IEventUpcaster<FixtureHopV1, FixtureHopV2>andIEventUpcaster<FixtureHopV2, FixtureHopV3>(DoubleHopUpcasterFixtures.cs:29-:30). It exists so the unique-source rule can be proven to reject a multi-hop upcaster class, not only two classes contesting one source. - Depends on - IEventUpcaster in both its non-generic and two-parameter forms, and three
internal sealedrung contracts declared in the same file:FixtureHopV1(default schema version,:8),FixtureHopV2(SchemaVersion => 2,:11-:14) andFixtureHopV3(SchemaVersion => 3,:17-:20), each a positional record over a singleSkuand derived from BaseIntegrationEvent. - Concept - cross-references the upcast-chain-as-a-function concept taught at the upcaster fixture family. The extra point this fixture carries is that an upcaster class must be exactly one hop. The runtime EventUpcasterRegistry indexes each instance by its single
SourceType(MMCA.Common/Source/Core/MMCA.Common.Application/Services/EventUpcasterRegistry.cs:65,:71). Declaring two generic contracts makes the two defaultSourceType/TargetTypeimplementations (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Events/IEventUpcaster.cs:72) conflict, so the compiler forces explicit non-generic members, and those can name only one hop. The class therefore reports V1 to V2 (DoubleHopUpcasterFixtures.cs:32-:34), routes the non-genericUpcastto the V1 overload (:40-:41), and the V2 to V3 rung silently disappears at runtime (fixture doc,:22-:28). Nothing about that shape fails to compile, which is why it needs a fitness rule.[Rubric §6, CQRS & Event-Driven]covers safe contract evolution;[Rubric §14, Testability]covers the adversarial fixture. - Walkthrough - two public typed
Upcastoverloads,FixtureHopV1 -> FixtureHopV2(:36) andFixtureHopV2 -> FixtureHopV3(:38), each copyingSkuacross, plus the three explicitIEventUpcastermembers described above. The rule finds it becauseEventUpcastersHaveUniqueSourceTypes(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:14) counts each class's"IEventUpcaster`2"interfaces by name and arity (:96-:98) and reports any class with more than one, with a message ending "an upcaster class must be exactly one hop" (:21-:23). That multi-hop check sits beside the contested-source check (:16-:19) and both feed the same violation list (:25). - Where it's used - named by
nameofinEventUpcasterFitnessTests.UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts, which asserts the rule's message contains the class name and the phrase"exactly one hop"(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:37-:44); see EventUpcasterFitnessTests. - Caveats / not-in-source - the fixture sits in the same
UpcasterFixturesnamespace the other upcaster facts read, so it is also present in the unique-source rule's output for the contested-contract fact; that fact asserts onlyContain/NotContainon specific names (EventUpcasterFitnessTests.cs:19-:24) and is unaffected.
FixtureLeakedPayload
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:20· Level 4 · record
- What it is - the domain-typed payload the payload-purity rule is built to catch: a nested record carrying an
AuditableBaseEntity<int>?reference, one level down from the event that holds it. - Depends on - AuditableBaseEntity<TIdentifierType> closed over
int(internal sealed record FixtureLeakedPayload(AuditableBaseEntity<int>? Entity);,PayloadPurityFixtures.cs:20). - Concept - the framework's payload-purity rule (ADR-010) requires an integration event crossing a transport boundary to carry primitives and DTOs only, because a subscriber on the far side has no shared
DbContextto resolve a leaked entity reference against. This fixture is nested one level inside its event on purpose:PayloadRule_FlagsADomainTypeReachedThroughANestedPayloadasserts the failure message names the full pathFixtureLeakingEvent.Payload.Entity, not justPayload, proving the rule walks nested payload shapes rather than stopping at the event's own properties (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:55-:57). - Where it's used - the sole property of the fixture event's
Payload, read byIntegrationEventPayloadPurityRuleTests.PayloadRule_FlagsADomainTypeReachedThroughANestedPayload(IntegrationEventPayloadPurityTests.cs:48-:59).
FixtureMap (Cqrs), FixtureMap (Governance)
MMCA.Common.Architecture.Tests.Cqrs and MMCA.Common.Architecture.Tests.Governance · (see per-type table) · Level 4 · class
Two unrelated one-entry maps that happen to share the plain name FixtureMap, both private sealed and nested in a different outer test class, so neither is visible outside its own file.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept introduced - each proves a different rule against a deliberately scoped fixture set, the same pattern taught at FixtureAssemblyMap: register only the layer(s) the rule under test reads, so the assertion is provable against a known shape rather than the real codebase. The Cqrs instance is a dual-layer map,
Module("Fixtures", Layer.Api, ...)plusModule("Fixtures", Layer.Application, ...)over the same assembly (ConstructorDependencyCountTestsBaseTests.cs:127-:82), becauseConstructorDependencyCountTestsBasereads bothMap.Api()andMap.ModuleApplication()to bound controller and handler constructors independently. The Governance instance is a single Shared-layerFramework(Layer.Shared, ...)map (FeatureFlagLifecycleTests.cs:108-:109) that stands in for the deliberately bad fixturesFeatureFlagsDeclareLifetimemust flag: an undeclared flag, a Permanent flag that also setsRemoveBy, a Temporary flag with noRemoveBy, and one whoseRemoveBydoes not parse as an ISOyyyy-MM-dddate. [Rubric §14 - Testability] assesses whether both rules are provable with focused inputs; [Rubric §34 - Architecture Governance and Documentation] is whyFeatureFlagsDeclareLifetimeexists (ADR-031's dead-toggle detector). - Walkthrough - both declare
RepoToken => "MMCA.Common"and a single-entryDefineLayers(); see the per-type table for the exact layer call each makes.
| Type | File:Line | Layer(s) | The rule it scopes |
|---|---|---|---|
FixtureMap (in ConstructorDependencyCountTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:121 |
Module("Fixtures", Layer.Api, ...) and Module("Fixtures", Layer.Application, ...), both anchored on typeof(FatFixtureController).Assembly (:81-:82) |
ConstructorDependencyCountTestsBase's controller and handler ceiling checks, via the TightCeilingTests/ConformantTests subclasses (:98,:110). |
FixtureMap (in FeatureFlagLifecycleRuleTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:104 |
Framework(Layer.Shared, typeof(FixtureBadFeatures).Assembly) (:109) |
ArchitectureRules.FeatureFlagsDeclareLifetime, via the private DeclarationFailure() helper (:96-:101), and TemporaryFeatureFlagsAreNotPastRemoveBy for the expiry facts (:72,:86-:87). |
- Why they're built this way - each is nested private inside the single test class that consumes it, keeping an eight-to-ten-line fixture map beside the assertions it serves rather than in a shared file whose scope a reader would have to look up. It is the same trade-off FixtureModuleMap and FixtureAssemblyMap make.
- Where they're used - constructed inline where each fact needs it:
new FixtureMap()insideTightCeilingTests/ConformantTests(ConstructorDependencyCountTestsBaseTests.cs:146,:112), andnew FixtureMap()inline at eachArchitectureRulescall inFeatureFlagLifecycleRuleTests(FeatureFlagLifecycleTests.cs:72,:86,:98).
StaleAllowListTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:171· Level 4 · class
- What it is - the adversarial fixture for the stale-entry half of the anonymous-endpoint gate: a subclass whose allow-list names an endpoint that does not exist, so the base's
AllowList_HasNoStaleEntriesfact must report it rather than shrug. - Depends on - AnonymousEndpointTestsBase (
private sealed class StaleAllowListTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:171). - Concept introduced - an allow-list rots in two directions, and only one of them is obvious. A missing entry fails loudly the moment a new
[AllowAnonymous]lands. A stale entry fails silently and permanently: the endpoint gets renamed, re-gated with[Authorize], or deleted, and the list keeps granting a permission that is no longer being requested. Nothing breaks, so nobody looks, and the next endpoint that happens to match that identifier inherits an approval nobody granted it. The base therefore runs the comparison both ways, computingstaleas the allow-list entries with no match in the scanned set (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:109-:89), with abecausethat spells out the consequence: an entry that no longer matches hides a renamed or re-gated endpoint behind a permission that is no longer being granted (:88). [Rubric §11 - Security] is the property; [Rubric §15 - Best Practices & Code Quality] is the mechanism, since a self-pruning list is one that stays readable. - Walkthrough -
TargetAssembliesis this test assembly (:110-:111), so the fixture controllers are discovered normally.AllowedAnonymousEndpointsholds exactly one entry,"MMCA.Common.Architecture.Tests.NoLongerAnonymousController.ReadAsync"(:113-:114), naming a controller that has never existed. That makes the fixture fail both facts at once (every real fixture endpoint is now an unlisted offender as well), which is harmless because the fact under proof invokes onlyAllowList_HasNoStaleEntriesas a delegate. - Where it's used - the subject of
Base_Fails_WhenTheAllowListHasAStaleEntry(AnonymousEndpointTestsBaseTests.cs:26-:35), which asserts the message contains"NoLongerAnonymous"(:33), the distinctive fragment of the phantom identifier.
StaleUndecoratedAllowListTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:199· Level 4 · class
- What it is - the adversarial fixture for the stale-entry half of the base's second override list,
EndpointsWithoutAuthorizationAttribute: a subclass that hand-names an endpoint that has never existed, so the staleness check on that list, distinct from the allow-list's own, is what must catch it. - Depends on - AnonymousEndpointTestsBase (
private sealed class StaleUndecoratedAllowListTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:199). - Concept - cross-references the two-directions-of-rot concept introduced at StaleAllowListTests, applied here to the strict-mode override list rather than the allow-list.
TargetAssembliesis this test assembly (:199-:200),AllowedAnonymousEndpointsis empty (:202), andEndpointsWithoutAuthorizationAttributeholds the one phantom entry,"MMCA.Common.Architecture.Tests.NoLongerUndecoratedController"(:204-:205). - Where it's used - the subject of
Base_Fails_WhenTheUndecoratedAllowListHasAStaleEntry(AnonymousEndpointTestsBaseTests.cs:107-:114), which assertsUndecoratedAllowList_HasNoStaleEntriesthrows with a message containing"NoLongerUndecorated"(:113).
StrictConformantTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:210· Level 4 · class
- What it is - the compliant fixture for the base's opt-in strict mode: a subclass with
RequireExplicitAuthorizationDecision => true(:215) whoseEndpointsWithoutAuthorizationAttributeis computed from the live scan itself,[.. UndecoratedEndpoints()](:217-:218), so it always names exactly what the scan would otherwise flag and never goes stale. - Depends on - AnonymousEndpointTestsBase (
private sealed class StrictConformantTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:210). - Concept - cross-references the strict-mode concept introduced at StrictDriftedTests. Where that fixture proves strict mode fails a genuinely undecorated controller, this one proves the same mode accepts an endpoint once it is reported and declared, and that inheritance still resolves correctly:
InheritingFixtureControllerand its abstract base are both absent fromUndecoratedEndpointsForTest()'s result (:91-:96), because the authorization decision was made once on the abstract base's action, exactly as ASP.NET Core resolves it. - Walkthrough - the internal
UndecoratedEndpointsForTest()helper (:220) exposes the protected base scan to the test class.Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBase(AnonymousEndpointTestsBaseTests.cs:86-:97) calls it directly;Base_Accepts_AnUndecoratedEndpointThatIsAllowListed(:99-:104) instead invokes the inheritedEndpoints_DeclareAnAuthorizationDecisionfact as a delegate and asserts it does not throw. - Where it's used - the subject of two facts in
AnonymousEndpointTestsBaseTests:Base_DoesNotReport_AControllerThatInheritsItsDecisionFromAnAbstractBaseandBase_Accepts_AnUndecoratedEndpointThatIsAllowListed.
StrictDriftedTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:189· Level 4 · class
- What it is - the adversarial fixture for the base's opt-in strict mode: a subclass with
RequireExplicitAuthorizationDecision => true(:194) that declares nothing in either override list, so a controller with no[Authorize]and no[AllowAnonymous]is invisible to neither list and the strict check must still catch it. - Depends on - AnonymousEndpointTestsBase (
private sealed class StrictDriftedTests : AnonymousEndpointTestsBase,AnonymousEndpointTestsBaseTests.cs:189). - Concept introduced - an opt-in strict mode for the gap the base allow-list alone cannot close.
[AllowAnonymous]'s absence is not the same claim as[Authorize]'s presence: a controller action that carries neither attribute is invisible to the plain allow-list scan, because nothing on it looks anonymous.RequireExplicitAuthorizationDecisionis the base's answer, a second scan that demands every action declare one or the other. [Rubric §11 - Security] is the property under test; [Rubric §14 - Testability] is why the fixture proves the failure path and not just the passing one. - Walkthrough -
TargetAssembliesis this test assembly (:189-:190),AllowedAnonymousEndpointsis empty (:192), andRequireExplicitAuthorizationDecisionis the only override beyond that (:194). - Where it's used - the subject of
Base_Fails_WhenAControllerCarriesNoAuthorizationDecisionAtAll(AnonymousEndpointTestsBaseTests.cs:74-:84), which invokesEndpoints_DeclareAnAuthorizationDecisionas a delegate and asserts it throws with a message namingUndecoratedFixtureController(:81).
RouteAuthorizationTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/RouteAuthorizationTestsBaseTests.cs:14· Level 4 · class
- What it is - the meta-test for the shipped route-authorization base: one seven-case theory proving the role matcher reads every
[Authorize]attribute on a page, plus two facts proving the governed-page assertion passes when the role is present among stacked or inherited attributes and still fails when it is absent. - Depends on -
RouteAuthorizationTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22), through three private probe subclasses declared in the same file:RoleProbe(RouteAuthorizationTestsBaseTests.cs:76),StackedAttributeTests(:87) andMissingRoleTests(:99). Externals: xUnit[Theory]/[Fact]and AwesomeAssertions. - Concept introduced - an attribute can be stacked or inherited, so a role gate must be matched across all of them.
[Authorize]allows multiple instances, and reflection does not promise which comes first, so a matcher that read only the first would see a bare[Authorize]and report a regression that is not one. The base'sRequiresRolechecks whether any authorize attribute carries the exact role (RouteAuthorizationTestsBase.cs:86-:91), andGovernedPages_RequireDeclaredRole(:50) fails the build for governed pages that lack it.[Rubric §11, Security]is the property (an admin route must not regress to one any signed-in user can reach);[Rubric §14, Testability]covers proving the matcher from both sides. - Walkthrough
RequiresRole_ReadsEveryAuthorizeAttribute(RouteAuthorizationTestsBaseTests.cs:56-:65): four pages expected true (a plain role gate, policy then role, role then policy, and a bare[Authorize]subclass over a role-gated base) and three expected false (bare only, a different role plus a bare attribute, and no authorize at all).RoleProbe.Requiresexposes the protected matcher (:120).GovernedPages_WithStackedOrInheritedAttributes_KeepTheirRoleGate(:67-:73): invokes the inherited fact as a delegate onStackedAttributeTests, which governs the three stacked or inherited pages (:131-:134), and asserts it does not throw.GovernedPages_WithoutTheRole_StillFail(:75-:82):MissingRoleTestsgovernsOtherRolePlusBarePage(:143) and the delegate must throw with that type name in the message, so the gate is not vacuous.
- Why it's built this way - the page fixtures are nested public types in the test class itself, each isolating one attribute order, so the theory data reads as a truth table. All three probes set
RequiredRoleto"Admin"(:54). - Where it's used - an independent class in the Common architecture suite; nothing references it outside its own file. Consumers subclass the base it protects.
UnmarkedOverwriteSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AnonymizationFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:16· Level 4 · class
- What it is - an
internal sealedIAnonymizablefixture whoseAnonymize()nullsNicknameand setsIsErasedbut carries no[Pii]marker, so it is the deliberately bad subject the privacy rule must flag (AnonymizationFixtures.cs:16-:34). - Depends on -
IAnonymizableandResult(Anonymize()returnsResult.Success()). - Concept introduced - the erasure method is the source of truth for what is personal data.
PropertiesOverwrittenByAnonymizeCarryPiisamples an instance, runsAnonymize()and reports any property it changed that is not marked[Pii]. The subject carries four decoys that pin the rule's scope:Nicknameis overwritten and unmarked (must be flagged),IsErasedis flipped by the erasure but is a flag, not personal data,Tagsis a collection (out of scope) andCityis never touched.AnonymizationRulesTestsasserts the message namesUnmarkedOverwriteSubject.Nicknameand does not nameIsErased,TagsorCity(AnonymizationRulesTests.cs:26,:38-:40).[Rubric §11, Security]and[Rubric §14, Testability]apply. - Walkthrough -
Nicknamehas a private setter (:21),Cityisinit-only (:23),Tagsexposes a read-only view of a private list (:27), andAnonymize()touches onlyNicknameandIsErased(:29-:34). - Where it's used - registered through the sample factory
static () => new UnmarkedOverwriteSubject()throughoutMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs(15 references there plus 14 more elsewhere per the usage index).
UnsampledSubject
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.AnonymizationFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AnonymizationFixtures/AnonymizationFixtures.cs:46· Level 4 · class
- What it is - an
internal sealedIAnonymizablewith an empty-successAnonymize()and no properties (AnonymizationFixtures.cs:46-:49). It exists to be anIAnonymizabletype that has no sample factory registered. - Depends on -
IAnonymizableandResult. - Concept - cross-references the erasure-as-source-of-truth concept at UnmarkedOverwriteSubject, covering the two coverage checks around it. The rule must report an
IAnonymizabletype missing from the sample set, and the assertion namesUnsampledSubjectby full name while not namingUnmarkedOverwriteSubject, which has a sample (AnonymizationRulesTests.cs:116-:122). The reverse rule reports a factory that returns a different type than the one it was registered for, with the messageUnmarkedOverwriteSubject: the sample factory returned UnsampledSubject(:93-:99). - Where it's used -
AnonymizationRulesTests.cs(6 references there plus 5 more elsewhere per the usage index), as the wrong-type factory result and as the deliberately unsampled type.
SingleLayerMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering.GovernedAi·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:36· Level 4 · class
- What it is - a private one-layer architecture map nested in AiBoundaryRulesTests: it takes any assembly through its primary constructor and registers it as the map's single Application layer, so the governed-AI rule scans exactly that assembly and nothing else.
- Depends on - ArchitectureMapBase (
private sealed class SingleLayerMap(Assembly assembly) : ArchitectureMapBase,AiBoundaryRulesTests.cs:36), the Layer enum and LayerRef;System.Reflection.Assemblyfrom the BCL. - Concept - cross-references the narrowed-map technique taught at CancellationTestMap. The difference is that this map is parameterized: where
CancellationTestMaphard-codes its own test assembly,SingleLayerMaptakes the assembly as a constructor argument, so one map type serves both the failing and the passing fact. Registering the assembly as Application is what puts it in scope at all, becauseGovernedAiPackageStaysBehindInfrastructureskips any assembly whose name contains.Infrastructureor names the AI package itself (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:79,:99-:107), and neither test assembly does. - Walkthrough -
RepoToken => "MMCA.Common"(AiBoundaryRulesTests.cs:38) and a one-entryDefineLayers()returningFramework(Layer.Application, assembly)(:40-:41). - Where it's used - constructed twice in its declaring class, once over
typeof(FixtureAiConsumer).Assembly(:19) and once overtypeof(ApiControllerBase).Assembly(:30); nothing outside the defining file references it.
SpanishAccentFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Localization·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:12· Level 4 · class
- What it is - the meta-test for SpanishAccentTestsBase: two facts that run the base's
Spanish_resources_keep_their_accentsfact over two fixture folders, proving the gate accepts correct Spanish that merely looks unaccented and still rejects a word that is missing its accent. - Depends on - SpanishAccentTestsBase through a private nested subclass
FixtureSpanishTests(SpanishAccentFitnessTests.cs:37-:40), ArchitectureMapBase forFindRepoRoot("MMCA.Common.slnx")(:14-:19), the two*.es.resxfiles underTests/Architecture/MMCA.Common.Architecture.Tests/SpanishFixtures/(Compliant/Compliant.es.resx,Offending/Offending.es.resx), and externals xUnit[Fact],Xunit.Sdk.XunitExceptionand AwesomeAssertions. - Concept - cross-references the thin-subclass gate taught at SpanishAccentTests; this class adds the false-positive side of the proof. The base deliberately leaves out words that are correct both with and without the accent depending on meaning, naming
aun("even" unaccented, "still" accented) among them (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:20-:24), whiletodaviastays in the default list (:34-:39). A gate that flagged correct Spanish would teach developers to bypass it, so the acceptance case is pinned as firmly as the rejection case.[Rubric §27, i18n]assesses whether localized text is correct;[Rubric §14, Testability]applies because the gate is proven from both sides against fixtures rather than against live resources. - Walkthrough
FixtureRoot(SpanishAccentFitnessTests.cs:14-:19) resolves the repo root and descends toTests/Architecture/MMCA.Common.Architecture.Tests/SpanishFixtures.UnaccentedAun_MeaningEven_IsAccepted(:21-:27) points the subclass atCompliant, whose single value isAun así, puede continuar.(Compliant.es.resx:4, the accentedistored as a character reference), and asserts the inherited fact does not throw.AWordMissingItsAccent_IsStillRejected(:29-:35) points it atOffending, whose value isLa solicitud todavia esta pendiente.(Offending.es.resx:4), and asserts anXunitExceptionwhose message contains"todavia". The same sentence carriesesta, which the base excludes on purpose, so only the one genuine mistake is required in the message.FixtureSpanishTests(string resourceRoot)(:37-:40) overrides onlyResourceRoot, so both facts run the base's defaults unchanged.
- Why it's built this way - the file's doc comment records that the nested subclass is private so xUnit does not discover it as a test class, and its fact is invoked directly over the fixture folders instead (
:6-:11). Feeding fixtures through the real base keeps the proof honest: the word list, the whole-word match and the<value>-only scan are all exercised exactly as consumers run them. See ADR-015. - Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.
SpanishAccentTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SpanishAccentTests.cs:11· Level 4 · class
- What it is - the framework's binding of the Spanish-accent fitness function: a five-line subclass that points the shared scan at MMCA.Common's own
Sourcefolder, so no*.es.resxstring ships a common word with its accent or n-tilde missing (codigo,sesion,contrasena). - Depends on - SpanishAccentTestsBase (
public sealed class SpanishAccentTests : SpanishAccentTestsBase,SpanishAccentTests.cs:11) and ArchitectureMapBase forFindRepoRoot("MMCA.Common.slnx")(:13-:14). - Concept introduced - a thin subclass of an authored-once gate. Those strings compile, render and pass every functional test, so a Spanish-speaking user is the only detector until a rule exists; the base supplies the scan and the subclass supplies only what differs per repo. The one abstract member is
ResourceRoot(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:46); this class overrides nothing else, so it takes the default word list (:32-:39), noAllowedEntries(:58) and aMinimumResourceFileCountof 1 (:64). The base reads every*.es.resxunder the root (:72-:76) and checks only each<value>text, matching whole words case-insensitively with a one-second regex timeout (:82-:86).[Rubric §27, i18n]assesses whether localized text is correct;[Rubric §34, Architecture Governance & Documentation]applies because the convention is executable. - Walkthrough -
ResourceRootisPath.Combine(ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx"), "Source")(SpanishAccentTests.cs:13-:14), a property initializer, so it resolves once per instance. The base's floor assertion fails the fact if that root holds no Spanish resources (SpanishAccentTestsBase.cs:79-:80). - Where it's used - collected and run by xUnit in the Common architecture suite; MMCA.ADC declares its own
SpanishAccentTestssubclass (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs), which is where repo-specific vocabulary or exceptions would go.
AiBoundaryRulesTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering.GovernedAi·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/GovernedAi/AiBoundaryRulesTests.cs:13· Level 5 · class
- What it is - the meta-test for the type-level governed-AI rule in ArchitectureRules: two facts proving
GovernedAiPackageStaysBehindInfrastructurereally fails a non-Infrastructure layer that names anMMCA.Common.AItype, and does not mistakeMMCA.Common.APIfor that package. - Depends on - ArchitectureRules (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Ai.cs:75), its own nested SingleLayerMap (AiBoundaryRulesTests.cs:36), the fixture FixtureAiConsumer (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/AiBoundaryFixtures/FixtureAiConsumer.cs:9), ApiControllerBase as the anchor for the API assembly (AiBoundaryRulesTests.cs:30), and externals xUnit[Fact]plus AwesomeAssertions. - Concept introduced - a fitness rule can pass vacuously, so the rule itself needs a test that it can fail. The class doc records the defect it pins (M257): the rule once matched the dotted entry
MMCA.Common.AI., which NetArchTest never matches because it compares whole namespace segments, so the gate passed in every subclass while checking nothing (AiBoundaryRulesTests.cs:7-:12). The rule now matches the undotted segment,GovernedAiPackageNamespace = "MMCA.Common.AI"(ArchitectureRules.Ai.cs:25, used at:83). Undotted raises the opposite risk, a prefix match catchingMMCA.Common.API, so the class pins both directions.[Rubric §16, AI-Native Application Architecture]assesses whether model access is confined behind a governed boundary; this class is what keeps that confinement check from silently going dead.[Rubric §34, Architecture Governance & Documentation]applies because a governance rule is only as good as its proof that it fires. - Walkthrough
ALayerNamingAGovernedAiType_FailsTheRule(AiBoundaryRulesTests.cs:15-:24): maps the test assembly as Application throughSingleLayerMap(typeof(FixtureAiConsumer).Assembly).FixtureAiConsumer.NameOf(PromptContract)takes anMMCA.Common.AItype as a parameter (FixtureAiConsumer.cs:14), which is all it takes to depend on the package. The fact asserts the message contains"must not reference MMCA.Common.AI"(the rule's text atArchitectureRules.Ai.cs:87) and namesFixtureAiConsumer.TheApiAssembly_IsNotMistakenForTheAiPackage(AiBoundaryRulesTests.cs:26-:33): maps theMMCA.Common.APIassembly as Application and asserts the rule does not throw, with the reason "API and AI are different namespace segments" (:32).
- Why it's built this way - both facts call the static rule directly over a one-assembly map instead of subclassing AiDependencyIsolationTestsBase, which invokes the same rule over a repo's full map (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:29). A single controlled assembly makes the expected outcome unambiguous. See ADR-120 and ADR-015. - Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.
AnonymizationRulesTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Privacy·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs:15· Level 5 · class
- What it is - the meta-test for the two anonymization rules in ArchitectureRules: eleven facts that prove the reverse rule (
PropertiesOverwrittenByAnonymizeCarryPii) reports exactly the unmarked overwrites and the coverage rule (AnonymizableEntitiesAreCoveredBySamples) reports exactly the unsampled entities. - Depends on - ArchitectureRules (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Anonymization.cs:12,:49), the shippedRefreshSessionentity (built byBuildRefreshSession,AnonymizationRulesTests.cs:162-:169), the fixture classes inAnonymizationFixtures/AnonymizationFixtures.cs(UnmarkedOverwriteSubject:16,FailingAnonymizeSubject:37,UnsampledSubject:46,AbstractAnonymizableSubject:52,NotAnonymizableSubject:58), andXunit.Sdk.XunitException, which each failing fact asserts by exact type. - Concept introduced - testing a behavioral privacy rule against a sample table. The reverse rule builds each sample, snapshots the text and reference properties declared on the type, runs
Anonymize(), and reports every property whose value moved but carries no[Pii](rule:49-:63,:65-:97); an erased value in an unmarked property would otherwise survive in the audit trail'sOldValuefor the retention window (rule doc:35-:38). Value types and collections are deliberately out of scope (:40-:46), whichReverseRule_IgnoresValueTypesCollectionsAndUntouchedPropertiespins (AnonymizationRulesTests.cs:41-:51). [Rubric §30 - Compliance/Privacy/Data Governance] applies, and [Rubric §14 - Testability] for the failure messages that name what to mark. - Walkthrough - a private
Samples(...)helper (:171-:172) turns tuples into the rule'sDictionary<Type, Func<object>>.- Reverse rule, eight facts (
:29-:110): an unmarked overwrite is reported with a message that says to add[Pii](:29-:38); a marked overwrite onRefreshSession(IpAddress,UserAgent) passes (:53-:61); a mixed table reports only the unmarked one (:63-:75); an empty table passes (:77-:80); and three malformed-sample facts name anAnonymize()that fails (:82-:90), a key that is not anonymizable (:92-:100), and a factory returning another type (:102-:110). - Coverage rule, three facts (
:113-:160): a concrete anonymizable type with no sample is named along with theAnonymizableSamplesoverride to extend, while an abstract type, a non-anonymizable type and a sampled type are not (:113-:135); full coverage passes (:137-:155); no anonymizable entity plus an empty table passes (:157-:160).
- Reverse rule, eight facts (
- Why it's built this way - the pair closes the gap in the forward
[Pii] => IAnonymizablescan: the reverse rule only checks entities someone sampled, so the coverage rule is what stops a new anonymizable entity from silently escaping it (rule doc:5-:9). Proving the NotThrow cases as deliberately as the failing ones keeps a repo with no anonymizable entity green. See ADR-005. - Where it's used - an independent class in the Common architecture suite; nothing outside the defining file references it.
AnonymousEndpointTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14· Level 5 · class
- What it is - the framework's own anonymous-endpoint allow-list: the live, reviewed statement of every controller action and routable page MMCA.Common ships without an authorization gate. Twenty-one entries today: the four credential-exchange endpoints, two password-recovery endpoints, two email-confirmation endpoints, four OAuth challenge/callback endpoints, six credential UI pages, and three landing/outcome pages that render nothing caller-dependent.
- Depends on - AnonymousEndpointTestsBase (
public sealed class AnonymousEndpointTests : AnonymousEndpointTestsBase,AnonymousEndpointTests.cs:14), ApiControllerBase as the anchor for the API assembly (:18), and UISharedAssemblyReference as the anchor for the shared UI assembly (:19). - Concept introduced - an allow-list as a review artifact, not a suppression list. The gate's value is not that it blocks
[AllowAnonymous]; it is that adding one becomes a line in a test file with a comment next to it, landing in a diff that a human reads. Every entry here carries its justification inline. Login, register and refresh mint or rotate the token pair, so requiring one would be circular (:24-:28); the OAuth completion code is the caller's only credential at that point and is single-use, burned on first exchange (:29-:31); forgot-password and reset-password are for a caller who has lost the credential, throttled by the same auth-ip rate-limit policy, and forgot-password always answers 202 so it reveals nothing about which addresses hold accounts (:32-:34); the two email-confirmation endpoints are followed from a mail client with no session, an unconfirmed account may be unable to sign in at all when confirmation is required, both are throttled by the auth-ip policy, and send always answers 202 on a well-formed request (ADR-116,:39-:41). The three OAuth challenge endpoints and the provider-callback completion run before any local token exists, so they declare their anonymity rather than relying on the absence of an attribute (:44-:46); the six credential UI pages (the confirm-email landing joins the five earlier ones) cannot already hold a token if the caller has to sign in, register or recover, andAuthorizeRouteViewreads attributes and ignores the fallback policy so these must declare themselves (:51-:55); the three landing and outcome pages render nothing that depends on the caller (:62). [Rubric §11 - Security] assesses the deliberateness of the authorization posture; note that the compensating control for every credential-exchange entry is rate limiting rather than authentication (ADR-029). [Rubric §9 - API & Contract Design] applies because the anonymous surface is part of the published contract, and [Rubric §34 - Architecture Governance & Documentation] because the reasoning lives in compiled code rather than a wiki page. - Walkthrough
TargetAssemblies(:16-:20) names two assemblies by anchor type, so the scan covers both the controllers and the routable Blazor pages the framework ships.AllowedAnonymousEndpoints(:22-:66) is the twenty-one-entry list. Two of them are worth reading closely:PasswordResetAuthControllerBase`2.ForgotPasswordAsyncand its reset sibling (:37-:38) carry a backtick-2 arity suffix, because PasswordResetAuthControllerBase<TForgotPasswordCommand, TResetPasswordCommand> is generic over the app's command records and reflection renders a generic type'sFullNamethat way. The comment above them says exactly that (:35-:36), which is the kind of note that saves the next person an hour.RequireExplicitAuthorizationDecision => true(:71) opts the framework's own controllers and pages into the StrictConformantTests/StrictDriftedTests strict gate: every concrete controller and every routable page in MMCA.Common must declare its authorization decision explicitly, so none of them depends on a reader noticing a missing attribute (:68-:70).MinimumScannedTypes => 21(:75) raises the base's default floor of 1 to this repo's known count, described in the comment as a floor and not an equality: 12 API controller types plus the routable UI pages (:73-:74). Removing a scanned type is therefore a failure rather than a quietly smaller scan.
- Why it's built this way - the endpoints that cannot require a token are exactly the ones an attacker reaches first, so the framework's position is that they must be enumerated, justified, and rate-limited rather than merely working. Opting into
RequireExplicitAuthorizationDecisionis the framework holding itself to the stricter of the two gates it ships, rather than only proving the gate works against fixtures. The base's own doc records the one thing this gate cannot see: minimal-API endpoints opt out through the.AllowAnonymous()builder call, which is endpoint metadata produced at map time and invisible to static reflection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:18-:24). The framework's minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is deliberate and small, but it is not covered here. - Where it's used - collected and run by xUnit in the Common architecture suite; its facts come entirely from the base. Consumers write their own subclass with their own list.
- Caveats / not-in-source - the minimal-API blind spot above is stated in the base's documentation, and closing it would need an endpoint-metadata check over a built host. Nothing in this class compensates for it.
CancellationTestMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:63· Level 5 · class
- What it is - a one-layer architecture map used only by CancellationTokenFitnessTests: it registers this test assembly as the map's single Application layer so the cancellation-token rule scans the fixture services and nothing else.
- Depends on - ArchitectureMapBase (
private sealed class CancellationTestMap : ArchitectureMapBase,CancellationTokenFitnessTests.cs:63), the Layer enum, and LayerRef. - Concept - cross-references the map concept from CommonArchitectureMap. The narrowing matters twice over here. First, the cancellation rule only visits the Application and Infrastructure layers (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:45-:47), so registering the test assembly as Application is what puts the fixtures in scope at all. Second, the rule's automatic exemption compares againstmap.Layers.Select(l => l.Assembly)(:42), so a map holding only this assembly is what makesIAsyncEnumerator<T>(declared in the BCL, outside the map) count as an externally-fixed contract. A wider map would not change that result here, but the single entry keeps the fixture's meaning unambiguous. - Walkthrough -
RepoToken => "MMCA.Common"(:65) and a one-entryDefineLayers()returningFramework(Layer.Application, typeof(CancellationTokenFitnessTests).Assembly)(:67-:68). - Where it's used - constructed on every call of the test class's private
RunRulehelper (:56).
CancellationTokenFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenFitnessTests.cs:12· Level 5 · class
- What it is - the meta-test for the trailing-
CancellationTokenrule: three facts that prove it flags a missing, misplaced, or misnamed token, leaves compliant and out-of-scope members alone, honors the caller-supplied exemption list, and never flags a signature the repo does not own. - Depends on - ArchitectureRules (the rule under test,
CancellationTokenFitnessTests.cs:55), its own CancellationTestMap (:56), the six fixture services inMMCA.Common.Architecture.Tests.CancellationFixtures(:1), and externals xUnit[Fact]plus AwesomeAssertions'Should().Throw<Exception>().Which(:59). - Concept introduced - asserting on a rule's failure message rather than on whether it threw. Every fixture in this file is deliberately in scope at once, so the rule always throws; a bare "it threw" assertion would prove nothing. Instead the private
RunRulehelper (:53-:60) captures the exception's message once and each fact makes positive and negativeContainassertions against that single string. The result is one rule execution that proves six independent behaviors, and every assertion carries abecauseclause spelling out the ruling it pins (:19-:24). [Rubric §14 - Testability] assesses whether guardrails are themselves trustworthy; this is the technique that makes a multi-branch classifier provable in three facts. [Rubric §12 - Performance & Scalability] is the property being defended, as taught in the fixture family section. - Walkthrough
Rule_FlagsMissingToken_ButNotCompliantOrNonAsyncMembers(:14-:25): asserts bothMissingTokenFixtureService.RunAsyncand.PingAsyncappear (:19-:22), and that neitherCompliantFixtureServicenor the internalHiddenAsyncdoes (:23-:24).Rule_FlagsMisplacedAndMisnamedTokens(:27-:36): asserts the two offender method names appear together with the rule's exact classifier strings,"must be the LAST parameter"(:33) and"must be named 'cancellationToken'"(:35), so the message a developer will actually read is pinned, not just the fact of a failure.Rule_HonorsExemptions_AndSignaturesTheRepoDoesNotOwn(:38-:51): runs the rule twice. Without exemptions,ExemptableFixtureService.LegacyAsyncis reported (:41-:43); with that one entry passed in (:45), it disappears (:47). The same run also assertsMoveNextAsyncnever appears (:48-:50), which is the automatic exemption rather than the explicit one.
- Why it's built this way - the rule is one of the framework's broadest (it judges every public awaitable method in two layers), so its false-positive behavior matters as much as its true-positive behavior. Pinning the message text also protects the developer experience: the classifier's wording is what tells someone how to fix the build. See ADR-015.
- Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CancellationTokenConventionTests.
EmptyScanTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:178· Level 5 · class
- What it is - the adversarial fixture for the constructor-dependency gate's non-vacuity guards: a subclass whose map's Api and Application layers hold neither a controller nor a handler, so both
NotBeEmptyguards inside the base's facts must fire. - Depends on -
ConstructorDependencyCountTestsBase(private sealed class EmptyScanTests : ConstructorDependencyCountTestsBase,ConstructorDependencyCountTestsBaseTests.cs:178) and its siblingBareMap(:87-:96), whose two layers both point attypeof(Common.Shared.Abstractions.Result).Assembly(:93-:94), a package with no controllers and no CQRS handlers. - Concept - cross-references the vacuity-guard concept the base class states directly in each fact:
Controllers_DoNotExceedConstructorDependencyCeilingassertscontrollers.Should().NotBeEmpty("the guard must scan at least one API controller (otherwise it passes vacuously)")(ConstructorDependencyCountTestsBase.cs:123-:95), andHandlers_DoNotExceedConstructorDependencyCeilingasserts the equivalent for handlers (:118-:119). A scan of nothing would otherwise report zero offenders and pass, which is indistinguishable from a healthy repo. [Rubric §14 - Testability] is the theme: the guard that proves the guard is not vacuous. - Walkthrough -
MapisBareMap(:123), and all three ceilings are left at theirConformantTests-matching values (int.MaxValue,3,3,:125,:127,:129) because the ceilings are irrelevant here: the fact must throw before ever comparing a count against them, on theNotBeEmptyguard alone. - Where it's used -
BothPopulations_FailVacuousScans(ConstructorDependencyCountTestsBaseTests.cs:60-:72) constructs oneEmptyScanTestsinstance and asserts bothControllers_DoNotExceedConstructorDependencyCeilingandHandlers_DoNotExceedConstructorDependencyCeilingthrow, each with abecauseclause stating what the empty scan would otherwise hide: "a map whose API assemblies hold no controller would pass while checking nothing" (:68-:69) and the handler equivalent (:70-:71).
ConformantTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:156· Level 5 · class
- What it is - the positive fixture for the constructor-dependency ceiling gate over controllers and handlers: a subclass whose ceilings sit exactly at the fixture population's widest constructor, so both facts must pass.
- Depends on -
ConstructorDependencyCountTestsBase(private sealed class ConformantTests : ConstructorDependencyCountTestsBase,ConstructorDependencyCountTestsBaseTests.cs:156, base atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:29) and its siblingFixtureMap(:75-:84), which points both the Api and Application layers of the map at this test assembly (Module("Fixtures", Layer.Api, ...)andModule("Fixtures", Layer.Application, ...),:81-:82). - Concept introduced - a ceiling is a ratchet, not a budget. The base class doc states the rule directly: a consumer sets its high-water mark at the current widest constructor so the next class cannot silently grow past it (
ConstructorDependencyCountTestsBase.cs:13-:17).ConformantTestsis the fixture that proves a ceiling set at that mark does not itself trip the gate, which is what distinguishes a ratchet from an arbitrary cap:MaxControllerConstructorDependencies => 3andMaxHandlerConstructorDependencies => 3(:116,:118) equal the exact dependency counts the siblingTightCeilingTestsfixture proves are offenders one below (TightCeilingTestsceilings of 2,:105,:107, flagFatFixtureController (3 ctor dependencies)and the two handler fixtures at the same count,ConstructorDependencyCountTestsBaseTests.cs:23,:44,:47). [Rubric §1 - Single Responsibility] is the property under test; [Rubric §14 - Testability] is why the fixture proves the boundary case rather than only the offending one. - Walkthrough -
Mapis the sharedFixtureMap(:112),MaxConstructorDependenciesis left atint.MaxValue(:114) because neither test that constructs this fixture exercises the Application-*Servicefact, only the controller and handler delegates read directly off the instance.MaxControllerConstructorDependencies => 3(:116) andMaxHandlerConstructorDependencies => 3(:118) are the two ceilings under proof. - Where it's used -
Controllers_AtTheCeiling_AreNotFlagged(ConstructorDependencyCountTestsBaseTests.cs:28-:34) readsnew ConformantTests().Controllers_DoNotExceedConstructorDependencyCeilingand assertsNotThrow;Handlers_AtTheCeiling_AreNotFlagged(:52-:58) does the same againstHandlers_DoNotExceedConstructorDependencyCeiling. Both assertions state the same ruling in theirbecauseclause: "the ceiling is a high-water mark: the widest controller/handler sits exactly on it" (:33,:57).
FitnessDependent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:48· Level 5 · class
- What it is - the other half of the specification-navigation fixture: an entity that holds a foreign-key scalar and a navigation to a FitnessPrincipal, so both a safe (scalar-only) and an unsafe (navigating) specification can be written over it.
- Depends on - AuditableBaseEntity<TIdentifierType> (
public sealed class FitnessDependent : AuditableBaseEntity<int>,SpecificationFitnessTests.cs:48) and FitnessPrincipal (thePrincipal?navigation,:52). - Concept introduced - cross-references the fixture concept introduced by FitnessPrincipal. This type adds the parts a specification can filter on:
PrincipalId(scalar FK,:50), a nullablePrincipalnavigation (:52), and aFlagscalar (:54). The scalar-versus-navigation split is the whole point: it lets one fixture support both the pattern the rule must flag and the pattern it must leave alone. - Walkthrough - three auto-properties:
int PrincipalId(:50),FitnessPrincipal? Principal(:52),bool Flag(:54). ScalarOnlySpec filters onPrincipalIdandFlag; NavigatingSpec reaches throughPrincipal. - Where it's used - the entity type parameter for all four fixture specifications in SpecificationFitnessTests: NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).
DbSetRemovingFixture, ExecuteDeletingFixture, SoftDeletingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.HardDeleteFixtures· (see per-type table) · Level 5 · class
Three internal static classes holding five one-line methods between them: three that erase rows through EF Core and two that do not. They are the compiled call sites the hard-delete rule has to tell apart.
- Depends on -
Microsoft.EntityFrameworkCoreforDbSet<T>and theExecuteDeleteAsyncqueryable extension (HardDeleteFixtures.cs:1) and FixtureEntity as the element type. - Concept - cross-references the hard-delete concept introduced by FixtureEntity. What this set proves is the disambiguation, which is the whole engineering content of the rule.
SoftDeletingFixture.ForgetcallsList<FixtureEntity>.Remove(:41), which is the same method name the rule bans, on a type the rule must ignore. Its own doc says why that matters (:32-:36):Removeon aList<T>is what makes the naive "ban the name Remove" version of this rule useless. Having a genuineList<T>.Removecompiled next to a genuineDbSet<T>.Removeis what turns the declaring-type check from an implementation detail into a proven behavior. - Walkthrough - the rule reports each site as
{type.FullName}.{method.Name} calls {DeclaringType.Name}.{callee.Name}(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:115), so the failure text names both ends. The allowlist is checked at the type level before any method is walked (:99-:102), which is why an entry silences a whole type rather than one call.
| Type | File:Line | What it models |
|---|---|---|
DbSetRemovingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:17 |
Two erasing members in one type: Purge calls DbSet<T>.Remove (:19) and PurgeMany calls DbSet<T>.RemoveRange (:21-:22). Both must be reported, which is how the test proves the rule walks every method rather than stopping at the first hit. |
ExecuteDeletingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:26 |
The server-side delete: PurgeAsync calls IQueryable<T>.ExecuteDeleteAsync (:28-:29). Kept in a separate type so the type-level allowlist can silence the DbSet fixture while this one still fails. |
SoftDeletingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:37 |
Two near-misses: Deactivate calls the framework's own entity.Delete() (:39), and Forget calls List<T>.Remove (:41). Neither is a hard delete, so the whole type name must be absent from the report. |
- Why they're built this way - two erasing types rather than one is what makes the type-level allowlist fact meaningful: exempting
DbSetRemovingFixturemust leaveExecuteDeletingFixturereported. Keeping all five methods to a single expression means the compiled IL contains the call under test and essentially nothing else. - Where they're used - scanned by SoftDeleteEnforcementFitnessTests through its FixtureAssemblyMap, which registers this assembly as the map's single Infrastructure layer.
DbSetRemovingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.HardDeleteFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:17· Level 5 · class
- What it is - the primary offender for the hard-delete rule: a static helper that erases rows through the
DbSetAPI's two removing members,RemoveandRemoveRange. - Depends on -
Microsoft.EntityFrameworkCore.DbSet<TEntity>(HardDeleteFixtures.cs:1) and its own FixtureEntity (:11). - Concept introduced - reading IL for the exact erasing member names, not for a type-level "calls DbSet somewhere" heuristic.
Purgecallsset.Remove(entity)(:19);PurgeManycallsset.RemoveRange(entities)(:21-:22). The rule must name both members independently in its failure message, which is whatHardDelete_ReportsEveryErasingMemberin SoftDeleteEnforcementFitnessTests pins (SoftDeleteEnforcementFitnessTests.cs:37-:38). - Walkthrough -
internal static class DbSetRemovingFixturewith two static methods,Purge(DbSet<FixtureEntity>, FixtureEntity)(:19) andPurgeMany(DbSet<FixtureEntity>, IEnumerable<FixtureEntity>)(:21-:22), each a one-line call into the EF member under test. - Where it's used - the subject of
HardDelete_OutsideTheAllowlist_IsFlagged,HardDelete_ReportsEveryErasingMember, and half ofAllowlistedType_SilencesOnlyThatTypein SoftDeleteEnforcementFitnessTests (SoftDeleteEnforcementFitnessTests.cs:21-:28,:30-:40,:62-:74); the last asserts an allowlist entry naming this type silences it while ExecuteDeletingFixture is still reported (:72-:73).
ErrorCatalogFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ErrorCatalogFitnessTests.cs:15· Level 5 · class
- What it is - the meta-test for the two error-catalog rules: eight facts that pin the cross-type collision, the two-branch near-miss, the shared-code exemption on both rules, the unprefixed offender, the
Error.TooManyRequestsfactory read into the catalog, the UNVERIFIABLE report for a dynamically-built code, and the distinct-code inventory. - Depends on - ArchitectureRules (
ErrorCodesAreUnique,ErrorCodesUseAnAllowedPrefixandDistinctErrorCodeCount,ErrorCatalogFitnessTests.cs:24,:58,:105), its nested FixtureModuleMap, the six fixture catalogs (:1), andXunit.Sdk.XunitException(:3). - Concept introduced - the third verdict: UNVERIFIABLE, and why a static analyser should have one. Most fitness functions are binary. These are not, because a code assembled at run time genuinely cannot be read out of the instruction stream, and both available binary answers are wrong: passing it hides a possible collision, failing it bans a legitimate pattern the framework never decided to ban. The rules take the third option and surface those sites in the failure message under an UNVERIFIABLE heading (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:155), so the reader learns the catalog has a blind spot and where it is. DomainThrowFitnessTests pins the same verdict for the same reason on a different rule. [Rubric §14 - Testability] assesses whether a guardrail is honest about its own limits, which is exactly what a named third verdict is; [Rubric §9 - API & Contract Design] is the property. - Walkthrough - a
private static readonly string[] SharedCodes = ["Error.NotFound"](:17) stands in for a repo's allowed-shared-codes list, andIsTicketsCode(:113) is the prefix delegate, closing over the fixture module's name exactly as a consumer's would close over its real module names.DuplicateCode_AcrossTwoTypes_IsFlagged(:21-:31): asserts the message names the code and both owning types (:28-:30), which is what makes a collision actionable.CodeReusedAcrossBranchesOfOneType_IsNotADuplicate(:33-:42): the near-miss, absent from the report because uniqueness is measured per declaring type.SharedCode_OnTheAllowList_IsExemptFromUniqueness(:44-:53) andPrefixedCodes_AndAllowedSharedCodes_PassThePrefixRule(:79-:89): the shared-code exemption proven against both rules, not just the one it was written for.UnprefixedCode_IsFlagged(:55-:64): asserts both the code and the owning type appear (:62-:63).TooManyRequestsCode_IsReadIntoTheCatalog(:68-:77):Error.TooManyRequestsis a code-first factory like its siblings, so its code (TooFast) and owning type (ThrottledErrors) must appear in the prefix report rather than escaping both gates unreported (:75-:76).DynamicCode_IsReportedAsUnverifiable(:91-:100): asserts the literal string"UNVERIFIABLE"and the owning type name are both present (:98-:99).DistinctCodeCount_CountsTheLiteralCatalog(:102-:110): the non-vacuity guard, asserted as a floor of 5 with the five literal codes enumerated in thebecause(:107-:109). A floor rather than an equality, because the count is over literals the scan can read and a future fixture addition should not be a test failure.
- Why it's built this way - both rules read the same collected sites, so proving them in one class against one fixture catalog keeps the two verdicts comparable, and each fact makes exactly one claim about the shared message. See ADR-015.
- Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of both rules is ErrorCatalogTestsBase, whose default prefix delegate closes over the repo's own module names.
ExecuteDeletingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.HardDeleteFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:26· Level 5 · class
- What it is - the second hard-delete offender: a static helper that erases rows with EF Core's server-side
ExecuteDeleteAsync, a distinct erasing path fromDbSet.Removebecause it never materializes or tracks the rows it deletes. - Depends on -
IQueryable<TEntity>.ExecuteDeleteAsync(HardDeleteFixtures.cs:1,:29) and its own FixtureEntity (:11). - Concept - cross-references the erasing-member concept introduced by DbSetRemovingFixture. This fixture proves the rule's IL scan catches a second, unrelated erasing API rather than one it happened to be written against:
PurgeAsynccallsquery.ExecuteDeleteAsync(cancellationToken)(:28-:29), which compiles to a different call site thanDbSet.Removebut must still be flagged by name. - Walkthrough -
internal static class ExecuteDeletingFixturewith one static method,PurgeAsync(IQueryable<FixtureEntity>, CancellationToken)(:28-:29), a one-line pass-through intoExecuteDeleteAsync. - Where it's used - named in
HardDelete_ReportsEveryErasingMember's"PurgeAsync"assertion (SoftDeleteEnforcementFitnessTests.cs:39) and inAllowlistedType_SilencesOnlyThatType, where it is the offender still reported after DbSetRemovingFixture alone is allowlisted (:72-:73).
SoftDeletingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.HardDeleteFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/HardDeleteFixtures/HardDeleteFixtures.cs:37· Level 5 · class
- What it is - the negative fixture for
HardDeletesOnlyInAllowedTypes: it deletes the framework's way and separately removes from an ordinary in-memoryList<T>, neither of which is a hard delete, so the rule must stay silent about both calls. - Depends on - FixtureEntity (
HardDeleteFixtures.cs:11), whose soft-deleteDelete()it calls (:39). - Concept introduced -
List<T>.Removeis what makes a naive "ban the method name Remove" version of this rule useless. The class doc states the point directly:Removeon aList<T>must not be confused withDbSet<T>.Remove, which DbSetRemovingFixture exercises as the positive case the rule must flag (:33-:36). - Walkthrough - two static methods:
Deactivate(FixtureEntity entity)callsentity.Delete(), the framework's soft-delete (:39);Forget(List<FixtureEntity> cache, FixtureEntity entity)callscache.Remove(entity)on the in-memory list, not aDbSet(:41). - Where it's used - registered as part of the Infrastructure layer FixtureAssemblyMap points at for
SoftDeleteEnforcementFitnessTests, whereHardDeletesOnlyInAllowedTypesmust not flag either call.
ChildlessFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:99· Level 5 · class
- What it is - an aggregate with a collection, but a collection of strings rather than child entities, so the cascade rule has nothing to cascade to even though the type never overrides
Delete(). - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class ChildlessFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:99). - Concept - cross-references the cascade-detection concept introduced by MissingOverrideFixture. Where that fixture is the true offender (children present,
Delete()not overridden), this one proves the rule reads the collection's element type and not merely its presence:Tagsis aList<string>(:101), so it is not a child-entity collection at all, and a naive "any collection plus no override" check would wrongly flag it. - Walkthrough - one private field,
private readonly List<string> _tags = [](:101), exposed asIReadOnlyCollection<string> Tags(:103). NoDelete()override. - Where it's used - the subject of
AggregateWithNoChildEntities_IsIgnoredin CascadeSoftDeleteFitnessTests (CascadeSoftDeleteFitnessTests.cs:62-:71), which asserts the rule's failure message does not name it (:68-:70).
ChildlessFixture, ExemptedOffenderFixture, HelperCascadingFixture, LoopCascadingFixture, MissingOverrideFixture, SelfOnlyDeleteFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures· (see per-type table) · Level 5 · class
Six aggregate roots in one file, every one an internal sealed class deriving AuditableAggregateRootEntity<TIdentifierType> over int, differing only in what (if anything) their Delete() override does with their child collection. They are the six-way truth table for the cascade-soft-delete rule.
- Depends on - AuditableAggregateRootEntity<TIdentifierType>, CascadeChildFixture, and Result (
CascadeFixtures.cs:1-:2). - Concept - cross-references the cascade concept introduced by CascadeChildFixture. What this set adds is the body half of the rule, and it is the sharper half. The rule reads the IL of the
Delete()override and accepts exactly two shapes: a direct call toDeleteChildren(the helper on the aggregate base,MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/AuditableAggregateRootEntity.cs:273), or a zero-argumentDeletereached through thecallvirtopcode, which is what a hand-rolledforeach (var line in _lines) line.Delete();compiles to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:172-:186). What it deliberately refuses to accept isbase.Delete(), and the reason is a small piece of C# trivia doing real work:base.X()on a virtual member is the one thing C# emits as a non-virtualcall, and nothing else in the language produces that pairing, so excluding it separates "delete myself" from "delete my children" without guessing (:178-:182). That single discrimination is what makes SelfOnlyDeleteFixture catchable at all. - Walkthrough - the rule reports two distinct reasons and names the offending collection in both, so the failure text tells a developer which member to fix:
"no Delete() override, so its children ({names}) stay active"(:160) and"Delete() never deletes a child, so its children ({names}) stay active"(:165). Allowlist entries are type full names or namespace prefixes, matched ordinally exactly as the hard-delete rule matches (:79-:83).
| Type | File:Line | What it models |
|---|---|---|
HelperCascadingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:23 |
The framework-idiomatic cascade: Result.Combine(DeleteChildren<CascadeChildFixture, int>(_children), base.Delete()) (:29-:30). Must pass. |
LoopCascadingFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:37 |
The hand-rolled cascade every aggregate used before the helper existed: a foreach calling each child's Delete() (:45-:48) then base.Delete(). Must also pass, which is what stops the rule from mandating one spelling. |
MissingOverrideFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:58 |
Owns _orphans and never overrides Delete() at all (:60-:62). The first violation reason, and the report must name _orphans. |
SelfOnlyDeleteFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:70 |
The subtle offender: it does override Delete(), and even touches the collection with _ignored.Clear() before calling base.Delete() (:76-:81). Detaching children from an in-memory list is not a soft delete, so every row stays active. Its doc names the point: this is the shape a naive "does it override Delete" check would pass, which is why the rule reads the body (:65-:69). |
ExemptedOffenderFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:88 |
Structurally identical to MissingOverrideFixture, kept separate so one fact can allowlist it and prove an exemption silences exactly the type it names while the other offender still fails (:84-:87). |
ChildlessFixture |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:99 |
The out-of-scope case: an aggregate with no Delete() override whose only collection is a List<string> (:101-:103). A list of strings is not a child-entity collection, so the rule must ignore the type entirely rather than report it. |
- Why they're built this way - three passing and three interesting failing cases, with the two offenders made structurally identical so the allowlist fact has an unambiguous control. Every fixture is
internaland namespaced, which is what keeps them invisible to the framework rules that run over CommonArchitectureMap and to the other self-tests in this assembly (file doc,:10-:15). See ADR-005 and ADR-015. - Caveats / not-in-source - the rule's documented limits apply to these fixtures too (
ArchitectureRules.CascadeSoftDelete.cs:84-:93): only direct calls in the override are read, so an aggregate that delegates the loop to a private helper is reported; the rule proves a cascade exists, not that it covers every collection; only fields declared on the aggregate are inspected; and grandchildren are the child's own cascade to own. No fixture here exercises those boundaries. - Where they're used - scanned by CascadeSoftDeleteFitnessTests through its FixtureAssemblyMap, and named by
nameofin all six of its facts.
ExemptedOffenderFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:88· Level 5 · class
- What it is - the same shape as MissingOverrideFixture (children present,
Delete()never overridden), kept as a separate type so one self-test can allowlist it by name and prove the exemption silences exactly that type and no other offender. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class ExemptedOffenderFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:88) and CascadeChildFixture (the child collection,:90). - Concept - cross-references the allowlist-precision concept introduced at ChildlessFixture's sibling assertions. Being a genuine offender with the identical shape as MissingOverrideFixture is the point: if the rule's allowlist matched by shape rather than by fully-qualified type name, allowlisting this type would silence both, and
AllowlistedType_SilencesOnlyThatTypewould have nothing to distinguish. - Walkthrough -
private readonly List<CascadeChildFixture> _exempt = [](:90), exposed asIReadOnlyCollection<CascadeChildFixture> Exempt(:92). NoDelete()override. - Where it's used - the subject of
AllowlistedType_SilencesOnlyThatTypein CascadeSoftDeleteFitnessTests (CascadeSoftDeleteFitnessTests.cs:73-:85): passing{FixtureNamespace}.{nameof(ExemptedOffenderFixture)}to the rule removes it from the failure message (:76,:83) while MissingOverrideFixture is still reported (:84).
HelperCascadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:23· Level 5 · class
- What it is - the positive fixture for the cascade-soft-delete rule
AggregatesCascadeSoftDeleteToChildren: an aggregate whoseDelete()override cascades through the framework's ownDeleteChildren<TChild, TId>()helper, so the rule must stay silent about it. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class HelperCascadingFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:23) and CascadeChildFixture (:25). - Concept introduced - cross-references the fixture concept in FixtureAssemblyMap: the rule reads the IL of
Delete(), so proving it accepts a shape means compiling a real override that calls the shared helper, not asserting against a description of one. - Walkthrough - a private
List<CascadeChildFixture> _children(:25) exposed read-only throughChildren(:27);Delete()combinesDeleteChildren<CascadeChildFixture, int>(_children)withbase.Delete()(:29-:30). - Where it's used - registered by FixtureAssemblyMap as part of the Domain layer for
CascadeSoftDeleteFitnessTests, whereAggregatesCascadeSoftDeleteToChildrenmust not flag it.
LoopCascadingFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:37· Level 5 · class
- What it is - a second positive fixture for
AggregatesCascadeSoftDeleteToChildren, cascading through a hand-rolledforeachloop instead of the framework helper: the shape every aggregate used beforeDeleteChildren<TChild, TId>()existed, which the rule must still accept. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class LoopCascadingFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:37) and CascadeChildFixture (:39). - Concept introduced - cross-references HelperCascadingFixture: together the two prove the rule reads the effect of
Delete()(every child'sDelete()gets called) rather than pattern-matching one specific call shape. - Walkthrough - a private
List<CascadeChildFixture> _items(:39) exposed throughItems(:41);Delete()iterates_items, callingitem.Delete()on each and discarding the result before returningbase.Delete()(:43-:51). - Where it's used - registered by FixtureAssemblyMap alongside HelperCascadingFixture for
CascadeSoftDeleteFitnessTests.
MissingOverrideFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:58· Level 5 · class
- What it is - the negative fixture for
AggregatesCascadeSoftDeleteToChildren: an aggregate that owns a child collection but never overridesDelete()at all, so deleting the root would leave every child row active. The rule must report it by name. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class MissingOverrideFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:58) and CascadeChildFixture (:60). - Concept introduced - cross-references FixtureAssemblyMap: the absence of an override is itself the offending shape, so this fixture has to compile with no
Delete()member at all rather than an empty one. - Walkthrough - a private
List<CascadeChildFixture> _orphans(:60) exposed throughOrphans(:62); noDelete()override anywhere in the type. - Where it's used - registered by FixtureAssemblyMap for
CascadeSoftDeleteFitnessTests, where the rule's failing fact names it.
SelfOnlyDeleteFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.CascadeFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CascadeFixtures/CascadeFixtures.cs:70· Level 5 · class
- What it is - the sharpest negative fixture for
AggregatesCascadeSoftDeleteToChildren: it overridesDelete(), even touches its child collection, and still leaves every child row active, because clearing an in-memory list is not a soft-delete. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class SelfOnlyDeleteFixture : AuditableAggregateRootEntity<int>,CascadeFixtures.cs:70) and CascadeChildFixture (:72). - Concept introduced - reading the override's body, not just its existence, is what the rule buys over a naive check. The class doc states this directly: "a naive 'does it override Delete' check would pass" this shape, which is why the rule reads the body (
:66-:68). - Walkthrough - a private
List<CascadeChildFixture> _ignored(:72) exposed throughIgnored(:74);Delete()calls_ignored.Clear()thenbase.Delete()(:76-:81), never callingDelete()on any child. - Where it's used - registered by FixtureAssemblyMap for
CascadeSoftDeleteFitnessTests, where the rule's failing fact names it alongside MissingOverrideFixture.
CommandValidatorCoverageFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CommandValidatorCoverageFitnessTests.cs:16· Level 5 · class
- What it is - the meta-test for the command-validation coverage rule: eight facts that pin each coverage shape, both escape hatches, and the inventory count that keeps the whole gate from passing vacuously.
- Depends on - ArchitectureRules (
CommandsHaveValidatorsandHandledCommandCount,CommandValidatorCoverageFitnessTests.cs:25,:103), its nested FixtureModuleMap, the thirteen types inMMCA.Common.Architecture.Tests.CommandValidatorFixtures(:1), andXunit.Sdk.XunitException(:3), which every fact asserts on by exact type rather than as a bareException. - Concept introduced - proving a rule and proving it is not vacuous, in the same class. Seven of the eight facts read the failure message of a rule that always throws, the same technique CancellationTokenFitnessTests uses. The eighth is different in kind:
CommandInventory_CountsTheDataCarryingCommandscallsHandledCommandCountand pins it at exactly 5 (:100-:108). That number exists because a map that resolved to no module Application assemblies would make every other fact in the class pass while reading nothing, and the same counter is what CommandValidatorCoverageTestsBase exposes to consumers as their own non-vacuity guard (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:73-:77). Pinning an exact count rather than a floor is affordable here because the fixture set is closed; a consumer uses a floor. [Rubric §14 - Testability] and [Rubric §24 - Forms/Validation/UX Safety] apply. - Walkthrough - one
private const string FixtureNamespace(:18) and one readonlyFixtureModuleMap(:20) are shared by every fact.CommandWithNoValidation_IsFlagged(:22-:31) andCommandWithItsOwnValidator_IsCovered(:33-:42): the offender and the direct-coverage case, asserted byContainandNotContainon the one message.CommandCoveredThroughTheRequestBridge_IsCovered(:44-:53) andBridgeWithNoRequestValidator_IsNotCoverage(:55-:64): the two halves of the bridge, with the secondbecausestating exactly why a registeredCommandRequestValidatoris not coverage on its own (:63).PayloadFreeCommand_IsSkipped(:66-:75): the marker command, absent from the report because it has no settable property to validate.AllowlistedCommand_IsExempt(:77-:89): passes one fully-qualified command name and asserts that command disappears while the other offender is still reported (:87-:88). That second assertion is what distinguishes a working exemption from a rule that stopped firing.AllowlistedNamespace_SilencesTheRule(:91-:98): passes the fixture namespace and assertsNotThrow, the only fact in the class where the rule is quiet.CommandInventory_CountsTheDataCarryingCommands(:100-:108): the count, with thebecauseenumerating the five commands by name.
- Why it's built this way - the escape hatch has two granularities (a type and a namespace) and both need proof, because a repo records "this command genuinely has nothing to check" at whichever level fits. Asserting on
XunitExceptionspecifically rather thanExceptionmeans an unexpected failure inside the rule (a null reference, a bad cast) fails the fact instead of being mistaken for the rule firing. See ADR-015. - Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the same rule is CommandValidatorCoverageTestsBase, which MMCA.Common itself does not subclass because it declares no modules.
CycleTestMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:52· Level 5 · class
- What it is - a one-layer map for the namespace-cycle fitness test, rooted at the
CycleFixturesnamespace so the rule sees only the three fixture namespaces and none of the real test code. - Depends on - ArchitectureMapBase (
private sealed class CycleTestMap : ArchitectureMapBase,NamespaceCycleFitnessTests.cs:52), LayerRef, and the Layer enum. - Concept introduced - constructing a
LayerRefdirectly to override the derived root namespace. Every other map in this chapter uses theFramework(layer, assembly)helper, which computes the layer's root namespace asMMCA.Common.{segment}(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95). That is wrong here, because the cycle rule groups types by the namespace segment directly beneathLayerRef.RootNamespace(ArchitectureRules.Cycles.cs:12-:16) and skips types outside it entirely: with a derived root the fixtures would not be grouped intoLeft/Right/Acyclicnodes at all. So this map builds theLayerRefby hand withFixtureRootas the fourth argument (:58-:62), which is the one place in the suite where the record's positional constructor (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31) is used directly. [Rubric §14 - Testability] is the theme: scoping the map is what keeps the assertion about the fixtures instead of about whatever else the test assembly happens to contain. - Walkthrough -
RepoToken => "MMCA.Common"(:54) and aDefineLayers()returning a singlenew LayerRef(string.Empty, Layer.Application, typeof(NamespaceCycleFitnessTests).Assembly, FixtureRoot)(:56-:63), whereFixtureRootis the class constant"MMCA.Common.Architecture.Tests.CycleFixtures"(:13). The empty first argument is the module name, which is what marks the layer as framework-owned rather than module-owned. - Where it's used - constructed once per fact in NamespaceCycleFitnessTests (
:18,:32,:42).
DataSubjectSample
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:79· Level 5 · class
- What it is - a representative "data subject" fixture: a single object that carries
[Pii]members alongside non-PII fields and implements an in-place erasure path, so the framework's privacy machinery can be exercised end to end against a realistic shape. - Depends on - IAnonymizable (it is
private sealed class DataSubjectSample : IAnonymizable,PiiErasureContractFitnessTests.cs:79), PiiAttribute (marksEmailandFullName,:91,:94), and Result (returned fromAnonymize,:99). - Concept introduced - closing a vacuous fitness function with a stand-in data subject. The framework's
[Pii] => IAnonymizablescan (PiiConventionTests) has nothing to assert because MMCA.Common ships no PII-bearing domain entity of its own. Rather than invent a fake aggregate in the Domain layer, this fixture models the exact contract a consumer PII aggregate (for example MMCA.ADC'sUser) must satisfy, and lets PiiErasureContractFitnessTests prove the three §30 mechanisms compose. [Rubric §30 - Compliance/Privacy/Data Governance] assesses whether erasure, redaction, and masking actually work together; this sample is the vehicle that keeps the framework's proof of that non-vacuous. - Walkthrough - public constants publish the expected before and after values so the tests can assert without magic literals:
SampleId = 7,PublicCity = "Atlanta",OriginalEmail,OriginalFullName(:81-:84), plus private anonymized placeholders (:86-:87).Id(:89) andCity(:97) are non-PIIinitpass-through fields;EmailandFullNameare[Pii]with private setters (:91-:95), which is what letsAnonymizerewrite them in place while no caller outside the type can.Anonymize()(:99-:105) overwrites both PII fields with the fixed placeholders and returnsResult.Success(); because it re-applies constants, calling it twice is idempotent by construction (:101). - Why it's built this way - ADR-005 (soft-delete versus erasure) requires that a right-to-erasure path be idempotent and leave no clear text behind. The fixture encodes that contract in the smallest object that can be pushed through
PiiRedactorandAnonymizetogether. - Where it's used - the sole fixture for PiiErasureContractFitnessTests.
FixtureAssemblyMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain· (see per-type table) · Level 5 · class
The name is used seven times in this assembly, once per IL-reading rule under proof, and all seven are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these seven rules starts from a different part of the map, so each map has to name a layer the rule reads or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through
ScannableAssemblyLocations(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule readsmap.OfLayer(Layer.Domain)and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:112), so registering Domain is mandatory there. The clock-read rule reads the Domain and Application layers together (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.ClockReads.cs:68-:69), so either would do and its map picks Domain; the read-repository rule reads Application only (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:87), so that entry is mandatory too. The substitutability rule scans every registered assembly when the map names no modules, which this module-less map does, so its Domain entry is nominal (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:95-:102). All seven useFramework(...)rather thanModule(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk byAssembly.Locationalso explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. - Walkthrough - each declares
RepoToken => "MMCA.Common"and a single-entryDefineLayers()naming one layer and one anchor type from the fixture namespace it serves. Anchoring bytypeof(SomeFixture).Assemblyrather than byAssembly.GetExecutingAssembly()is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
| Type | File:Line | Layer and anchor | The rule it scopes |
|---|---|---|---|
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 |
Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) |
AggregatesCascadeSoftDeleteToChildren. |
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 |
Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) |
HardDeletesOnlyInAllowedTypes. |
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 |
Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) |
DomainEventHandlersDoNotSave. |
FixtureAssemblyMap (in DomainThrowFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 |
Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:156) |
DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing. |
FixtureAssemblyMap (in ClockReadTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:98 |
Framework(Layer.Domain, typeof(InjectedClockFixture).Assembly) (:104) |
DomainAndApplicationDoNotReadTheClock. |
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 |
Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) |
QueryHandlersUseReadRepositories, which reads the Application layer only, so this entry is load-bearing. |
FixtureAssemblyMap (in SubstitutabilityRuleTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:73 |
Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly) (:79) |
SubstitutableMembersDoNotRefuseTheContract, a module-less map so the rule scans the fixtures. |
- Why they're built this way - seven nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
- Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Three of the original six are unaffected because their fixtures are the only matching shapes present. The domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (
DomainThrowFitnessTests.cs:29-:34), including the substitutability fixtures that deliberately throwNotSupportedException. The clock-read and read-repository tests take the other way out: they assert on what the failure report names rather than on the report being otherwise empty, and say so in their class docs (ClockReadTests.cs:10-:12,QueryHandlerReadRepositoryTests.cs:10-:12). - Where they're used - constructed once as a readonly field per test class (
CascadeSoftDeleteFitnessTests.cs:21,SoftDeleteEnforcementFitnessTests.cs:19,DomainEventHandlerSaveFitnessTests.cs:21,DomainThrowFitnessTests.cs:36,ClockReadTests.cs:18,QueryHandlerReadRepositoryTests.cs:18,SubstitutabilityRuleTests.cs:20). The first four and the seventh pass it to every rule call; the last two pass it through a singleReporthelper that turns the rule's failure into a message string (ClockReadTests.cs:88,QueryHandlerReadRepositoryTests.cs:48), and keep the real CommonArchitectureMap as their base class'sMap.
NavigationContractTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/NavigationContractTests.cs:19· Level 1 · class
- What it is - a documentation-drift gate for navigation (rubric §25): it asserts that the "Routes shipped by the framework" table in
NavigationFlow.mdstays in lockstep with the routable pages theMMCA.Common.UIassembly actually ships, that each route's documented auth posture matches the[Authorize]reality on the page, and that every route parameter carries a type constraint. - Depends on - UISharedAssemblyReference (the reflection anchor for the shared UI assembly,
NavigationContractTests.cs:105), plus externals: ASP.NET CoreRouteAttributeandAuthorizeAttribute(the route and guard metadata reflected over,:107,:113), two source-generated regexes via[GeneratedRegex]partial properties (:139,:142), the embeddedNavigationFlow.mdmanifest resource (:125; wired by the csproj atMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/MMCA.Common.Architecture.Tests.csproj:11-:13), xUnit[Fact], and AwesomeAssertions. - Concept introduced - documentation as an executable contract. This is the first place a test asserts a hand-authored markdown doc against live code reality rather than the reverse.
NavigationFlow.mdlives next to the framework code (it is an embedded resource of this test project, perMMCA.Common/CLAUDE.md) precisely so this gate can parse it; a page added, removed, or re-routed without the doc moving in the same change fails the build, and so does an auth-posture lie in either direction. [Rubric §25 - Navigation & IA] assesses whether routing and information architecture are deliberate and documented; this test turns the route table into a build-enforced invariant instead of review discipline, and its fourth fact holds the typed-deep-link half of the same category. [Rubric §11 - Security] and [Rubric §26 - Front-End Security] also apply: a route the doc calls Authenticated must carry[Authorize], and a route it calls Anonymous must not, so a mis-documented guard cannot pass silently. [Rubric §34 - Architecture Governance & Documentation] is the umbrella: the documentation is proven current by CI. - Walkthrough - two constants pin the contract:
MinimumRoutes = 8(:21) andDocResource = "NavigationFlow.md"(:22). Four[Fact]s enforce it.RoutablePages_AreDiscovered_GateIsNotVacuous(:24-:28) asserts reflection finds at least eight routed pages, so a broken anchor cannot let the whole gate pass having scanned nothing.EveryRoutablePage_IsDocumented_AndEveryDocumentedRoute_Exists(:30-:43) computes the set difference both ways:undocumentedroutes (real but missing from the doc,:36) andphantomroutes (documented but no longer real,:37) must both be empty.EveryDocumentedAuthPosture_MatchesTheRouteAttributeReality(:45-:79) walks each documented route, skips ones the set-equality fact already reports as phantoms (:54-:57), classifies the auth cell asAuthenticated...,Anonymous, orAny(:59-:61), and flags three violation kinds: an unrecognized posture string (:63-:66), a doc that promises authentication where the page carries no[Authorize](:67-:69), and a doc that promises an open route where the page is actually guarded (:71-:73).RouteTemplateParameters_AllCarryTypeConstraints(:81-:100) matches every{...}segment of every discovered template withRouteParameterRegexand fails any whose body carries no:(:90-:94). The violation text states the ruling: an unconstrained parameter accepts arbitrary strings, so the page rather than the router becomes the validation boundary, where a constrained one renders NotFound before the component is reached.- Two private helpers supply the two sides.
DiscoverRoutedPages(:102-:121) reflects over the shared-UI assembly, collecting eachRouteAttribute.Templateand whether the type carries[Authorize](inherited attributes included,:113);DiscoverDocumentedRoutes(:123-:137) reads the embedded doc, throwing a clearInvalidOperationExceptionwhen the resource is missing (:125-:126), and extracts route rows via the source-generatedRouteRowRegex(:139-:140, a 2000ms-timeout[GeneratedRegex]partial property).
- Why it's built this way -
NavigationFlow.mdis deliberately kept in MMCA.Common next to the routed pages (not in the Website docs library) so this gate can embed and parse it; making route and auth-posture documentation a compiled assertion is what keeps a public navigation contract from rotting. ADR-015 establishes the fitness-function approach this class applies to documentation. - Where it's used - an independent class in the
MMCA.Common.Architecture.Testssuite; it runs in CI'sbuild-and-testjob (fast, no database) and has no Store or ADC counterpart because it guards the framework's own shared-UI route table. It is also one of the two entries DomainThrowFitnessTests allowlists, because its missing-resource guard is a realInvalidOperationExceptionthrow inside this assembly (DomainThrowFitnessTests.cs:31).
SortableColumnFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/SortableColumnFitnessTests.cs:15· Level 5 · class
- What it is - the meta-test for the MudDataGrid sortable-column rule: four facts proving it reports a sortable
TemplateColumnwith its file and line, catches the same defect written as a Razor expression, stays silent on every near-miss, and fails loudly on a missing scan root. - Depends on - ArchitectureRules (
SortableGridColumnsUsePropertyColumn,SortableColumnFitnessTests.cs:31), ArchitectureMapBase (FindRepoRoot,:18), four real.razorfixture files underRazorFixtures/, andXunit.Sdk.XunitException(:2). - Concept introduced - scanning markup text, and the near-misses that decide whether a text scan is usable. MudBlazor's server-side sort reads the bound property off a
PropertyColumn; aTemplateColumnhas no bound property, so marking oneSortable="true"produces a clickable header that sorts nothing, or sorts only the page's local slice. The failure is silent: the grid renders, the arrow toggles, and the order is wrong (rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:10-:15). The defect lives in markup that compiles perfectly, so this is the one rule in the suite that reads.razortext rather than IL or reflection (:16-:19). That choice imports every classic text-scanning hazard, and the rule addresses each one: the tag is read to its closing>through quoted attribute values, so attribute order, wrapped lines and a genericT="Foo<Bar>"argument all work (:22-:29); theSortablevalue is matched quoted or unquoted, with or without an@expression prefix and parentheses, while a value bound to a field is left alone because its value is not knowable from markup; and@* *@comments are blanked with spaces before the scan, newlines preserved, so a commented example neither fails the gate nor shifts the reported line numbers (:31-:34,:82-:108). [Rubric §18 - UI Architecture] and [Rubric §23 - Front-End Performance] both apply, since server-side sorting is what keeps a grid from paging in memory; [Rubric §14 - Testability] is the technique. - Walkthrough -
FixtureRootis built fromArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx")plus the four path segments down toRazorFixtures(:17-:22), withOffendingandCleanas expression-bodied properties over it (:24,:26).SortableTemplateColumn_IsFlaggedWithFileAndLine(:28-:37): asserts the message containsSortableTemplateColumnGrid.razor:7. That exact number is the point: in the fixture the element opens on line 7 and theSortable="true"attribute sits on line 8 (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/RazorFixtures/Offending/SortableTemplateColumnGrid.razor:7-:9), and the rule must report where the element starts, not where the attribute happens to land.ExpressionBoundLiteralTrue_IsAlsoFlagged(:39-:48): pinsExpressionSortableTemplateColumnGrid.razor:5, a one-line element withSortable="@(true)"written first (RazorFixtures/Offending/ExpressionSortableTemplateColumnGrid.razor:5).ConformingMarkup_Passes(:50-:57): the discrimination fact, and the one carrying the most fixture.ConformingGrid.razorpacks seven near-misses into one file: a sortablePropertyColumn(:6), aTemplateColumnwith noSortableat all (:9), one explicitlySortable="false"(:16), one bound to a field (:19), a longer attribute nameIsSortable="true"(:22), a generic argument carrying an angle bracket inside a quoted value with a real sortablePropertyColumnon the line below it (:26-:27), and a longer element nameTemplateColumnGroup(:30).CommentedOutGrid.razorholds the eighth: the whole defect inside a@* *@block (:3-:9).MissingRoot_FailsRatherThanScanningNothing(:59-:70): points the rule at a folder that does not exist and asserts the message contains"markup root not found"(:67), which is this rule's version of the non-vacuity floor. A path typo must fail the gate rather than quietly making it pass.
- Why it's built this way - the fixtures are real
.razorfiles rather than inline strings because the rule takes directory roots and enumerates files itself, excludingbinandobj(ArchitectureRules.Markup.cs:71-:76); testing it through anything but the filesystem would be testing a different method. The generic-argument case (ConformingGrid.razor:24-:27) is the sharpest of the seven: its own comment records the failure being prevented, a tag scan that runs past the element and swallows the sortablePropertyColumnbelow it. - Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is SortableColumnConventionTestsBase, which supplies the repo's real markup roots.
- Caveats / not-in-source - a
Sortablevalue bound to a field or property is deliberately not judged, so aTemplateColumnwhose binding evaluates totrueat run time passes this gate.ConformingGrid.razor:19is that shape, listed as conforming.
NamespaceCycleFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleFitnessTests.cs:11· Level 5 · class
- What it is - the meta-test for the namespace-acyclicity rule: three facts that prove it reports the two-namespace fixture cycle, leaves the acyclic fixture namespace out of the report, accepts the cycle when the allowance covers every namespace on it, and still fails when the allowance covers only part of it.
- Depends on - ArchitectureRules (
ArchitectureRules.NamespacesHaveNoDependencyCycles,NamespaceCycleFitnessTests.cs:18), its nested CycleTestMap, the three fixture namespaces underCycleFixtures(LeftModelBase, LeftService, RightModel, AcyclicConsumer), and externals xUnit plus AwesomeAssertions. - Concept introduced - why a cycle allowance must cover the whole component, not just a name on the path. The rule takes an optional
allowedCycleNamespaceslist and skips a cycle only when every namespace on it is listed (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:39-:44,:61). The reason is subtle and worth internalizing: if a partial allowance sufficed, a fourth namespace joining an already-accepted tangle would slip in silently, because the reported shortest path might not even mention it (:57-:60). The third fact in this class is the executable proof of exactly that policy. [Rubric §15 - Best Practices & Code Quality] and [Rubric §7 - Microservices Readiness] are the properties defended; [Rubric §34 - Architecture Governance & Documentation] covers the allowance mechanism, which forces an accepted tangle to be written down in code rather than tolerated in review. - Walkthrough - a class constant
FixtureRoot = "MMCA.Common.Architecture.Tests.CycleFixtures"(:13) is shared by the map and every assertion, so the namespace strings cannot drift apart.Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces(:15-:26): asserts the message contains both...CycleFixtures.Leftand...CycleFixtures.Right(:21-:22) and not...CycleFixtures.Acyclic(:23-:25), each with abecausenaming the edge that put it there.Rule_Passes_WhenTheWholeCycleIsAllowed(:28-:36): passes both namespaces as the allowance and assertsNotThrow()(:31-:35). This is the only fact in the class where the rule does not throw.Rule_StillFails_WhenOnlyPartOfTheCycleIsAllowed(:38-:46): passes onlyLeftand asserts it still throws, with the reason stated inline as "a partial allowance must never hide a cycle" (:45).
- Why it's built this way - the allowance is the rule's only escape hatch, and an escape hatch that can be misused silently is worse than none. Proving both its accepting and its refusing behavior is what lets the framework's own NamespaceCycleTests carry a real three-namespace allowance without weakening the gate.
- Where it's used - an independent class in the Common architecture suite.
ProtoContractFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14· Level 5 · class
- What it is - the meta-test for the frozen-
.protocontract gate: seven facts that prove a pinned proto matches its committed snapshot, that a renumbered field and an added rpc are both reported, that a missing file fails loudly, that the parser ignores the lines that are not part of the wire contract, that block comments in any position do not hide declarations, and that members behind an rpc body or inside one-line message and enum bodies are still pinned. - Depends on - ArchitectureRules (
ProtoContractsMatchFrozenList,ProtoContractFitnessTests.cs:47, andBuildProtoContract,:93), ArchitectureMapBase (FindRepoRoot,:92), three fixture.protofiles underTestData/, and externals xUnit plus AwesomeAssertions. - Concept introduced - a wire contract as a committed, sorted string snapshot. A
.protofile is a published contract: a renumbered field silently breaks every deployed peer, because protobuf keys on the number and not the name. The rule renders each file into a canonical, ordered list of lines (one per rpc with its streaming flag, one per message field with its label, type and NUMBER, one per enum value) and compares that list to a frozen array checked into the test (:28-:42). Regenerating the snapshot is a deliberate act: you printBuildProtoContractand paste the result, which is what makes a contract change a reviewable diff rather than a silent one. [Rubric §9 - API & Contract Design] assesses whether published contracts are versioned and change-controlled; this is the gRPC half of that discipline, and it is the same technique the integration-event contract snapshot uses (IntegrationEventContractTestsBaseTests proves that side). [Rubric §7 - Microservices Readiness] applies because these are exactly the contracts that survive extraction (ADR-007). - Walkthrough - three constants locate the fixtures relative to the repo root:
SolutionFileName = "MMCA.Common.slnx"(:16), theTestDatadirectory (:18), and the pinned and drifted file arrays (:20,:22). The 12-lineFrozenContract(:28-:42) is the snapshot, sorted so the comparison is order-independent for the author and deterministic for the diff.Rule_PassesWhenTheProtoMatchesItsFrozenSnapshot(:44-:50): the positive case.Rule_FlagsARenumberedField(:52-:64): asserts both sides of the change are reported, the newproduct_id = 7line as unexpected (:58-:60) and the frozenproduct_id = 1line as missing (:61-:63), which is what makes a renumbering legible in the failure output.Rule_FlagsAnAddedRpc(:66-:74): asserts the addedDeleteProductrpc is named, because an addition peers have not been told about is still a contract change.Rule_ReportsAMissingProtoFileExplicitly(:76-:87): a path typo must produce<missing proto file> does-not-exist.protorather than silently pinning an empty contract, the classic way a snapshot gate rots into a no-op.BuildProtoContract_IgnoresSyntaxImportAndOptionLines(:89-:98): resolves the repo root, builds the contract from the pinned file, and asserts it equalsFrozenContractexactly and contains neither thecsharp_namespaceoption nor thetimestamp.protoimport (:95-:97). Those lines are code-generation details, not wire contract, so churning them must not fail anyone's build.BuildProtoContract_SurvivesBlockCommentsInEveryPosition(:100-:110): builds the contract fromfitness-catalog-commented.proto(:104-:105) and asserts it equals the sameFrozenContract(:107-:109). That fixture carries a block comment closed on its own line, a trailing one after a field, a/*inside a line comment, and a block spanning two lines (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/TestData/fitness-catalog-commented.proto:17,:19,:20,:24-:25). A comment stripper that miscounts any of them swallows the declarations after it, which would shrink the contract and surface as a spurious "missing" diff, so comments are proven inert the same way the option and import lines are.Parser_PinsMembersBehindAnRpcBodyAndInsideOneLineBodies(:112-:128): builds the contract fromfitness-shapes.proto(:117-:118) and asserts exactly six lines: twoKindenum values,Reply.kindandReq.id, and bothShapeServicerpcs (:121-:127). The closing brace of an rpc option body must not pop the service (otherwiseSecondwould vanish), and a one-line message or enum body must still pin its members (comment at:112-:113).
- Why it's built this way - MMCA.Common ships no protos of its own, so this is the framework's only exercise of a consumer-facing rule (class doc,
:9-:12); consumers subclass ProtoContractTestsBase. The pinned and drifted fixture files differ by exactly the two breaking changes under test (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/TestData/fitness-catalog.protoversusfitness-catalog-drifted.proto), and the pinned one deliberately includes a nested message, a nested enum, arepeatedfield, anoptionalfield, and a streaming rpc so the parser is exercised on every shape it claims to handle. The commented file renders to the identical snapshot, so the one frozen array serves all three. - Where it's used - an independent class in the Common architecture suite. None of the three fixture protos is compiled by any project.
ReadRepositoryFixtureAggregate
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ReadRepositoryFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ReadRepositoryFixtures/ReadRepositoryFixtures.cs:10· Level 5 · class
- What it is - the compiled-in aggregate
QueryHandlerReadRepositoryTestsruns its handler fixtures against: an entity with no members of its own, present only soIUnitOfWork.GetRepository<T, int>()andGetReadRepository<T, int>()have a real type parameter to close over. - Depends on - AuditableAggregateRootEntity<TIdentifierType> (
internal sealed class ReadRepositoryFixtureAggregate : AuditableAggregateRootEntity<int>,ReadRepositoryFixtures.cs:10). - Concept introduced - the rule under test reads which repository call a handler's IL makes, not what the repository returns. None of the three handler fixtures ever executes; the file's own header states it (
:8-:9). WriteRepositoryQueryHandlerFixture callsunitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>()after anawait Task.Yield()(:22-:23), which is the shape the read-repository rule must flag in a query handler even once the method has become a compiler-generated state machine; the sibling read-repository fixture callsGetReadRepository<ReadRepositoryFixtureAggregate, int>()instead (:34), the shape the rule must accept. - Walkthrough - a single-line sealed class with no members:
internal sealed class ReadRepositoryFixtureAggregate : AuditableAggregateRootEntity<int>;(:10). - Where it's used - the entity type parameter for every repository call in WriteRepositoryQueryHandlerFixture, ReadRepositoryQueryHandlerFixture and WriteRepositoryCommandHandlerFixture (
:23,:34,:45), all in the same file; not referenced outside it.
SliceCohesionFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionFitnessTests.cs:17· Level 5 · class
- What it is - the meta-test for the vertical-slice-cohesion rule
ArchitectureRules.HandlersAreCoLocatedWithTheirContracts: four facts that prove an abstract handler stranded away from its contract's namespace is flagged, the same shape sitting beside its contract is not, a handler over a generic type-parameter contract is skipped outright, and a handler serving two stranded contracts is reported for both. - Depends on -
ArchitectureRules.HandlersAreCoLocatedWithTheirContracts(SliceCohesionFitnessTests.cs:24,:35,:46,:59), its nested FixtureApplicationMap held in the_mapfield (:19), and theMMCA.Common.Architecture.Tests.SliceFixturesfixture handlers (StrandedFixtureHandlerBase,GetFixturePreferencesHandlerBase,GetFixtureEntityHandlerBase<TQuery>,FixtureTwoContractHandlerBase) and their contractsGetFixturePreferencesQuery,GetFixtureFarQueryandGetFixtureSecondFarQuery, plus externals xUnit and AwesomeAssertions. - Concept introduced - an abstract base is scanned on the same terms as a concrete handler, but only when its contract is a concrete same-assembly type, and every contract a handler serves is judged on its own. The rule this class exercises checks the command/query and handler for a shared namespace and exempts a contract that is a generic type parameter, since
ICommandHandler<TCommand, ...>andIQueryHandler<TQuery, ...>bases are the framework's own generic handlers and cannot be co-located with every contract they might someday serve (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Slices.cs:27-:33). Because registration wires a class for every closed handler interface it implements, the rule loops over every contract of each handler and reports each stranded one (ArchitectureRules.Slices.cs:35-:37, the inner loop at:46-:57). This class proves all three properties: an abstract base whose contract IS a concrete same-assembly type is checked like any handler (StrandedFixtureHandlerBase), one whose contract is a type parameter (GetFixtureEntityHandlerBase<TQuery>) is exempt regardless of where it lives, and a class implementing two handler interfaces (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/FixtureTwoContractHandlerBase.cs:12-:13) is flagged once per stranded contract, not only for whichever interface reflection lists first. [Rubric §5 - Vertical Slice Architecture] is the property; [Rubric §14 - Testability] is the technique, an IL/namespace scan that only compiling the shapes side by side can prove. - Walkthrough - all four facts build the rule call as an
actdelegate over the same_mapand read the thrown message, so none of them needs a passing map of its own.AnAbstractHandlerStrandedFromItsContract_IsFlagged(:21-:30): asserts the message containsnameof(StrandedFixtureHandlerBase), with the reason stated inline, an abstract base is the shape consumers derive from, so a base stranded away from its contract strands every handler built on it.AnAbstractHandlerBesideItsContract_IsNotFlagged(:32-:41): asserts the message does NOT containnameof(GetFixturePreferencesHandlerBase), because the base and the query it serves share one namespace, exactly what the rule asks for.AnAbstractHandlerOverAGenericContract_IsSkipped(:43-:52): asserts the message does NOT containnameof(GetFixtureEntityHandlerBase<>), since a contract that is a type parameter is not a type any folder can hold, so the framework's own generic handler bases stay exempt.AHandlerServingTwoStrandedContracts_IsFlaggedForBoth(:54-:64, tagged L242 in its comment): captures the thrown message once and asserts it contains bothnameof(GetFixtureFarQuery)andnameof(GetFixtureSecondFarQuery)(:62-:63), the two contractsFixtureTwoContractHandlerBaseserves from one namespace away (both declared underSliceFixtures/Stranded/).FixtureApplicationMap(:66-:78): a private nestedArchitectureMapBasewithRepoToken => "MMCA.Common"and a singleFramework(Layer.Application, typeof(GetFixturePreferencesQuery).Assembly)layer (:74-:77), registering this test assembly as the Application layer the rule reads its handlers from.
- Why it's built this way - the rule always throws against this map, so all four facts read the same message shape without any of them constructing a passing map, the same discipline IdempotencyFitnessTests uses for the same reason: one deliberately-offending fixture set keeps the assertions honest about which handler or contract is named.
- Where it's used - an independent class in the Common architecture suite; its fixtures live in
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SliceFixtures/(SliceFixtures.cs,FixtureTwoContractHandlerBase.csand theStranded/subfolder). The shipped binding of the same rule over real code is SliceCohesionTestsBase.
FormsConventionTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionTestsBaseTests.cs:11· Level 5 · class
- What it is - the self-test for the password-form rule inside FormsConventionTestsBase: it feeds the rule's two public static helpers,
MissingPasswordFormMarkersandRendersChangePasswordCard, plus the sharedMissingMarkersthey build on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:138; the two helpers at:102,:248), hand-built markup so each marker can be shown to fire on its own. - Depends on -
FormsConventionTestsBaseandArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx")(FormsConventionTestsBaseTests.cs:45), see ArchitectureMapBase. - Concept - prove each marker by deleting exactly one.
CompleteForm(:13-:19) is a markup string with anErrorSummarybound to_passwordForm?.Errors, three required fields and both password validators.CompletePasswordForm_HasNoMissingMarkers(:22) asserts it is clean; every other fact removes one ingredient withReplaceand asserts the helper names the loss: no summary (:26), a field no longer required, which drops both the requiredness and the required message below three (:32), and no match validation, which leaves the single entryValidateConfirmPassword(:53). [Rubric §14 - Testability] is the theme: a markup-scanning rule that could not be seen to fail would pass vacuously. - Walkthrough -
SharedChangePasswordCard_KeepsEveryPasswordFormMarker(:42) reads the realSource/Presentation/MMCA.Common.UI/Components/Auth/ChangePasswordCard.razorand requires an empty marker list, because a consumer Profile page that renders the shared card passes the base's Profile fact on that card's strength (:37-:40). The commented-out facts (:58-:85, tagged M238) pin that a guard wrapped in a Razor@* *@or HTML<!-- -->comment no longer renders, so it must not count: a commentedErrorSummary(:63-:67) and a commented required field (:69-:73) are flagged on the password form, and a commentedUnsavedChangesGuardis flagged byMissingMarkerswhile an uncommented one after a note passes (:75-:85). The theoryRendersChangePasswordCard_IgnoresCommentedOutTags(:87-:94) pins that a@* *@or<!-- -->comment around the tag does not count as rendering it, while a tag after a comment does. - Where it's used - run by the test assembly only; nothing outside the defining file references it.
ForwardedJwtAudienceFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Api·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs:15· Level 5 · class
- What it is - the self-test for the fail-closed audience rule
ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed(sourceRoots, minimumCalls)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25), run over two on-disk fixture trees instead of the real hosts. The shipped-host run is the abstractForwardedJwtAudienceTestsBase(.../Bases/Api/ForwardedJwtAudienceTestsBase.cs:29). - Depends on -
ArchitectureRules,ArchitectureMapBase.FindRepoRootfor theJwtAudienceFixturesroot with itsCleanandOffendingfolders (ForwardedJwtAudienceFitnessTests.cs:17-:22), andJwtAudience.RequireConfigured(MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Auth/JwtAudience.cs:28). - Concept - a text-matching rule needs both a counted positive and named negatives.
ConformingCall_Passes_AndNearMissesAreNotCounted(:29) passes withminimumCalls: 1and throws with2, proving the clean fixture holds exactly one real call because its comment, documentation mention and declaration do not count.FallbackInsteadOfTheGuard_IsFlaggedWithFileAndLine(:40) andFallbackMaskedInsideTheGuard_IsFlagged(:49) require the failure to nameFallbackProgram.cs:4andMaskedProgram.cs:5(a??fallback inside the guard).MissingRoot_FailsRatherThanScanningNothing(:58) requires "source root not found" so a mistyped path cannot pass by scanning nothing. - Walkthrough -
RuleLiteral_MatchesTheShippedGuard(:66) comparesnameof(JwtAudience.RequireConfigured)to the string"RequireConfigured", because the rule matchesJwtAudience.RequireConfigured(by text: renaming the guard fails this fact until the rule moves with it. - Where it's used - run by the test assembly; the fixture programs are
JwtAudienceFixtures/Clean/Program.cs,Offending/FallbackProgram.csandOffending/MaskedProgram.cs.
FixtureLeakingEvent
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.PayloadPurityFixtures.IntegrationEvents·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/PayloadPurityFixtures/IntegrationEvents/PayloadPurityFixtures.cs:12· Level 5 · record
- What it is - the deliberately bad fixture for the integration-event payload-purity rule: an event whose payload reaches a domain entity one level down, through a nested record, so the rule can be proven to fire rather than merely asserted to pass (
PayloadPurityFixtures.cs:6-:10). - Depends on - BaseIntegrationEvent (
:12) and its nested payload FixtureLeakedPayload, which is itself typed onAuditableBaseEntity<int>(:20). - Concept introduced - a leak one level down is the one a reviewer misses. The payload rule's assertion in
IntegrationEventPayloadPurityRuleTests.PayloadRule_FlagsADomainTypeReachedThroughANestedPayloadchecks the full dotted pathFixtureLeakingEvent.Payload.Entityin the failure message, not just the event's name (IntegrationEventPayloadPurityTests.cs:55-:57), and separately asserts the leaked assembly (MMCA.Common.Domain) appears too (:58). Nesting the domain type insidePayloadrather than putting it directly on the event is what makes that path assertion meaningful: a shallow leak would pass a rule that only reads top-level properties. - Walkthrough - a single nullable
FixtureLeakedPayload? Payloadinit property (:15); the type carries no other members.FixtureLeakedPayload(AuditableBaseEntity<int>? Entity)(:20) is the domain-typed nested record that completes the leak. - Where it's used - both
FrameworkProbeMapandModuleProbeMapinIntegrationEventPayloadPurityRuleTestsregister it as the sole type in their Shared layer (IntegrationEventPayloadPurityTests.cs:83,:92), andPayloadRule_LeavesACleanEventAloneasserts the sibling FixtureCleanEvent is absent from the same failure message (:66-:69), which is what proves the rule names only the offender.
IdempotencyFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:12· Level 5 · class
- What it is - the meta-test for the POST idempotency-intent gate: three facts that prove the rule names the undeclared controller, stays silent about the three declared ones, and skips abstract controllers entirely.
- Depends on - ArchitectureRules (
ArchitectureRules.PostActionsDeclareIdempotencyIntent,IdempotencyFitnessTests.cs:17), its nested IdempotencyTestMap and four controller fixtures, IdempotentAttribute and NonIdempotentAttribute (:2), plusMicrosoft.AspNetCore.Mvc(:1), xUnit, and AwesomeAssertions. - Concept introduced -
nameofinstead of string literals in message assertions. Every assertion here is built fromnameof(UndeclaredFitnessController)and friends (:21,:32,:35,:38,:48), so renaming a fixture is a compile-time event rather than a silently-passing test. Combined with the always-throwing design (CancellationTokenFitnessTests uses the same shape), it means the suite fails loudly on refactors instead of drifting. [Rubric §14 - Testability] and [Rubric §15 - Best Practices & Code Quality] both apply. - Walkthrough
Rule_FlagsThePostThatDeclaresNothing(:14-:23): calls the rule through anactdelegate, captures the thrown exception, and asserts the message containsUndeclaredFitnessController.CreateThingAsync(:20-:22). Note it asserts the qualified member, so the fact fails if the rule reports a type without its offending action.Rule_AcceptsDirectAndInheritedAndOptedOutDeclarations(:25-:40): threeNotContainassertions covering the direct declaration (:31-:33), the inherited one (:34-:36), and the justified opt-out (:37-:39), each with abecauseclause stating the ruling.Rule_SkipsAbstractControllers(:42-:50): assertsAbstractFitnessControllerBaseis absent, which is theConcreteClassesfilter's doing, not an attribute's.
- Why it's built this way - one deliberately-offending fixture means the rule always throws, so all three facts can read the same message shape without any of them having to construct a passing map. The gate itself exists because an undeclared POST is a duplicate-write incident waiting to happen (ADR-015 for the enforcement approach).
- Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is IdempotencyConventionTests.
IdempotencyTestMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyFitnessTests.cs:52· Level 5 · class
- What it is - a one-layer map that registers this test assembly as the Api layer, which is the only layer the POST idempotency rule scans.
- Depends on - ArchitectureMapBase (
private sealed class IdempotencyTestMap : ArchitectureMapBase,IdempotencyFitnessTests.cs:52), the Layer enum, and LayerRef. - Concept - cross-references the map concept from CommonArchitectureMap. The layer choice is the whole configuration:
PostActionsDeclareIdempotencyIntentstarts frommap.OfLayer(Layer.Api)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:48), so pointing the Api layer at the test assembly is what substitutes the fixture controllers for the framework's real ones. - Walkthrough -
RepoToken => "MMCA.Common"(:54) and a one-entryDefineLayers()returningFramework(Layer.Api, typeof(IdempotencyFitnessTests).Assembly)(:56-:57). The root namespace the helper derives (MMCA.Common.API) is unused by this rule, which scans whole assemblies rather than namespace-scoped subsets. - Where it's used - constructed once per fact in IdempotencyFitnessTests (
:17,:28,:45).
IdentifierFixtureMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:53· Level 5 · class
- What it is - a one-layer map that registers this test assembly as the Shared layer, which is the only layer
StronglyTypedIdsAreReadonlyRecordStructsscans. - Depends on - ArchitectureMapBase (
private sealed class IdentifierFixtureMap : ArchitectureMapBase,StronglyTypedIdFitnessTests.cs:53), the Layer enum, and LayerRef. - Concept - cross-references the map concept from CommonArchitectureMap: the layer choice is the whole configuration, so pointing the Shared layer at the test assembly is what substitutes the fixture id wrappers for the framework's own strongly typed ids.
- Walkthrough -
RepoToken => "MMCA.Common"(:55) and a one-entryDefineLayers()returningFramework(Layer.Shared, typeof(StronglyTypedIdFitnessTests).Assembly)(:57-:58). - Where it's used - constructed once per fact in StronglyTypedIdFitnessTests: the two facts that flag a non-record and a mutable wrapper (
:16,:26), the fact that flags extra state (:36), and the fact that pins the two compliant shapes are silent (:46).
LayerDependencyOverrideTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyOverrideTests.cs:18· Level 5 · class
- What it is - six facts pinning the per-module escape on the map-completeness rule
ModulesDeclareLayers: a thin module fails under the strict default, passes when the override names its own layers, and every other module stays strict while one is overridden. - Depends on - ArchitectureRules (
ModulesDeclareLayersin both overloads,LayerDependencyOverrideTests.cs:30,:43), its nested FakeArchitectureMap, LayerRef and the Layer enum. - Concept introduced - why an escape hatch is per-module rather than a trimmed default.
ModulesDeclareLayersis the rule that stops every other per-module layer rule from passing vacuously: it fails when a repo forgets to register a module's assembly in its map (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:82-:88). That creates a real problem for a deliberately thin module, one that owns no aggregate and no persistence, so no Domain and no Infrastructure assembly exists to register. Before the override existed there was exactly one way to express that: trim the default list, which silently stops enforcing those layers for every other module in the repo. One thin module would buy blanket permission to forget an assembly anywhere (rule doc,:96-:104). The three-argument overload (:113) instead takes a per-module dictionary and replaces the required list for the modules it names, leaving everything else at the strict default (:131-:137). Naming the exception keeps the weakening exactly where it is true. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are declared and enforced; [Rubric §34 - Architecture Governance & Documentation] is the mechanism, since the exception ends up written in code with a reason next to it rather than silently absent; [Rubric §14 - Testability] is why an override needs its own test at all, since an escape hatch that is too wide is indistinguishable from a disabled rule. - Walkthrough - a static
FiveLayerslist (:20-:21) is the required set every fact starts from, and two helpers build maps from it:Full(module)yields all five layers,Thin(module)yields only Shared, Application and Api (:117-:121).ModulesDeclareLayers_FailsForAThinModule_WhenEveryModuleIsHeldToTheFullList(:23-:34): the baseline, asserting the exact message"*module 'Notification' declares no Layer.Domain*"(:33).ModulesDeclareLayers_PassesForAThinModule_WhenTheOverrideNamesItsOwnLayers(:36-:52): the same map with an override naming Notification's three real layers, assertedNotThrow.ModulesDeclareLayers_KeepsEveryOtherModuleStrict_WhileOneIsOverridden(:54-:73): the load-bearing fact. Both modules are thin and only one is overridden, so the other must still fail (:71-:72). Its inline comment states the whole reason the override exists rather than a trimmed default list (:57-:58).ModulesDeclareLayers_OverrideCanAlsoDemandMore_NotJustLess(:75-:92): overrides a full module with the five layers plusLayer.Uiand asserts it now fails (:87,:91). Pins that the override replaces rather than subtracts, so a repo can hold one module to a stricter bar than the rest.ModulesDeclareLayers_TwoArgumentOverloadIsUnchanged(:94-:104) andModulesDeclareLayers_NullOverrides_HoldEveryModuleToTheDefaultList(:106-:115): the two backward-compatibility facts, since the two-argument overload is what every current consumer calls (comment,:96-:98) and it forwardsoverrides: null(ArchitectureRules.Layers.cs:89-:90).
- Why it's built this way - the map is a fixture rather than any real repo's, because the point under test is the rule's arithmetic over module names and layers, not any particular assembly (class doc,
:14-:16). Asserting on the exact message withWithMessagerather than on a bare throw is what pins which module and which layer got reported, which is the only part of the output a developer acts on. - Where it's used - an independent class in the Common architecture suite. The rule it guards is called by LayerDependencyTestsBase (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:57), which passes the subclass'sModuleRequiredLayerOverrides; MMCA.Common's own LayerDependencyTests declares no modules, so the rule is vacuous there and the consumers are the real users.
LooseCeilingTests
MMCA.Common.Architecture.Tests |
MMCA.Common.Architecture.Tests.Cqrs|MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:167| Level 5 | class
- What it is - a private nested fixture inside ConstructorDependencyCountTestsBaseTests: a
ConstructorDependencyCountTestsBasesubclass whose controller and handler ceilings are set one ABOVE the widest fixture class (4 each, against three declared dependencies), so the tightness facts must flag both populations. - Depends on - ConstructorDependencyCountTestsBase (the abstract base it extends,
ConstructorDependencyCountTestsBaseTests.cs:167), the nestedFixtureMap(:121-:130, built asMapat:169), andint.MaxValueforMaxConstructorDependencies(:171) so the Application*Servicefact stays out of play. - Concept introduced - a ceiling that is merely not exceeded is a budget; a tight ceiling is a ratchet.
MaxControllerConstructorDependencies => 4(:173) andMaxHandlerConstructorDependencies => 4(:175) pass the exceed-the-ceiling facts trivially, yet leave one dependency of slack. The*_ConstructorDependencyCeilingIsTightfacts on the base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:181,:194) fail it and tell the author to lower the ceiling "from 4 to 3" (:226), so the next class cannot grow into the slack. [Rubric section 15 - Best Practices & Code Quality] is the property; [Rubric section 14 - Testability] is the technique. - Walkthrough - four overrides:
Map(:169),MaxConstructorDependencies(:171), and the two ceilings (:173,:175). - Why it's built this way - a third ceiling value beside the tight (2) and exact-fit (3) subclasses completes the set of cases: below, at, and above the observed maximum.
- Where it's used - two facts in ConstructorDependencyCountTestsBaseTests:
LooseControllerCeiling_IsFlagged_NamingPopulationCeilingAndObservedMaxandLooseHandlerCeiling_IsFlagged. Private to that file, not used elsewhere.
InlineStyleFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui.ComponentConventions·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleFitnessTests.cs:12· Level 5 · class
- What it is - the meta-test for the no-inline-styles design-system rule: it pins the line detector spelling by spelling, then proves the directory scan reports file, line and attribute, passes clean markup, honors prefix allowances, and fails rather than passing vacuously when it scans too few files or a missing root.
- Depends on - ArchitectureRules (
InlineStyleViolationsandRazorMarkupCarriesNoInlineStyles,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:84,:37), ArchitectureMapBase (FindRepoRoot, used to buildFixtureRoot,InlineStyleFitnessTests.cs:14-:20), theOffendingandCleanRazor fixtures underRazorFixtures/InlineStyles, and externals xUnit plus AwesomeAssertions. - Concept introduced - a markup rule is pinned at two levels: the one-line detector, and the scan around it. The detector is a regex for an attribute or parameter whose name ends in
styleorStyle(optionallyStyleFunc) assigned with=followed by a double quote, a single quote or@(so a single-quoted value and a bare Razor expression count), behind a lookbehind that keepsdata-styleand CSS text such aslist-styleout (ArchitectureRules.InlineStyles.cs:127). Lines inside an@codeor@functionsblock are C#, not markup, so a second regex skips them by brace depth and scanning resumes with the markup after the block (:131,:93). The scan adds the guards that stop a moved root from making the gate vacuous: a missing root is a violation (:52), fewer thanminimumRazorFilesfiles is a violation (:66-:68), and eachallowedViolationsentry exempts every violation that starts with it, file orfile:line(:61). [Rubric section 20] is the property; [Rubric section 14 - Testability] is the technique. - Walkthrough -
FixtureRootis resolved from the repo root (:14-:20).Detector_FlagsEveryInlineStyleSpelling(:22-:31): a six-row theory (style=,Style=,CellStyle=,HeaderStyle=,RowStyleFunc=, andStyle =with spaces) each yielding exactly one finding that names the fixture fileSample.razor, line 1 and the matched attribute.Detector_FlagsSingleQuotedAndExpressionValuedStyles(:33-:42): a five-row theory (style='...',Style=@_style,Style = @(CardStyle(n)),CellStyle='...',RowStyleFunc=@RowStyle), each yielding one finding, so quote style and Razor expressions cannot dodge the rule.Detector_SkipsTheCodeBlock_AndKeepsScanningMarkupAfterIt(:44-:65): C# variables namedstyle,rowStyleandcellStyleinside two@codeblocks (brace-on-same-line and brace-on-next-line forms) are ignored, while the markup<div style=...>between them is the only finding, astyle=hit on line 7 of theSample.razorfixture file.Detector_LeavesNearMissesAlone(:67-:73):class=,data-style=,list-style-noneandCellClass=produce nothing.OffendingMarkup_IsFlaggedWithFileLineAndAttribute(:75-:86): theOffendingfixture is reported asInlineStyled.razor:2 style=through:5 RowStyleFunc=.ConformingMarkup_Passes(:88-:94): theCleanfixture does not throw.AllowListEntry_ExemptsOnlyThePrefixItNames(:96-:106):InlineStyled.razorexempts the whole file,InlineStyled.razor:2leaves the other lines flagged.TooFewFiles_FailsRatherThanPassingVacuously(:108-:114): demanding 2 files from a 1-file root fails with "scanned 1 .razor file(s), expected at least 2".MissingRoot_FailsRatherThanScanningNothing(:116-:122): a nonexistent folder fails with "markup root not found".
- Why it's built this way - the detector is public (
ArchitectureRules.InlineStyles.cs:84) precisely so each spelling can be pinned line by line without fixture files; the file-based facts then only need to prove the plumbing. - Where it's used - an independent class in the Common architecture suite; no usage outside its defining file.
StronglyTypedIdFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdFitnessTests.cs:11· Level 5 · class
- What it is - the meta-test for the strongly-typed-id fitness rule: four facts that prove it flags a wrapper that is not a record, one that is not readonly, and one carrying extra state beyond its wrapped value, while staying silent about the two compliant declarations.
- Depends on -
ArchitectureRules.StronglyTypedIdsAreReadonlyRecordStructs, its nested IdentifierFixtureMap, five fixture wrapper types underStronglyTypedIdFixtures(NotARecordFixtureId, MutableFixtureId, ExtraStateFixtureId, CompliantFixtureId, CompliantStringFixtureId), and externals xUnit plus AwesomeAssertions. - Concept introduced - the opt-in strongly-typed-id shape is a structural contract, not a naming convention. ADR-115 makes a wrapper two lines,
public readonly record struct OrderId(int Value) : IStronglyTypedId<OrderId, int>plus itsFromfactory, and this rule is what enforces the shape rather than trusting it by convention: the wrapper must be a record (so equality is structural), must bereadonly(so it behaves like the primitive it wraps), and must carry no member beyond the wrapped value (so nothing hides state EF and JSON binding do not know about). [Rubric §4 - DDD] is the property, a strongly typed id is a value object at the identifier boundary; [Rubric §15 - Best Practices & Code Quality] is the discipline of failing the build rather than the code review when the shape drifts. - Walkthrough - all four facts call the same rule against the same map, so the class carries no shared fixture beyond that map.
Rule_FlagsAWrapperThatIsNotARecord(:27-:35): asserts the message namesNotARecordFixtureIdand contains "record".Rule_FlagsAWrapperThatIsNotReadonly(:37-:45): assertsMutableFixtureIdand "readonly".Rule_FlagsAWrapperCarryingExtraState(:47-:55): assertsExtraStateFixtureIdand "Label", the name of the offending extra member.Rule_SaysNothingAboutTheCanonicalDeclaration(:57-:65): the discrimination fact. It asserts the message does NOT containCompliantFixtureIdorCompliantStringFixtureId, so the other three facts are proven against a rule that can also stay silent, not one that always throws.IdentifierFixtureMap(:67-:73): a private nestedArchitectureMapBasewithRepoToken => "MMCA.Common"and a singleFramework(Layer.Shared, typeof(StronglyTypedIdFitnessTests).Assembly)layer, so the rule scans this test assembly's own fixture types.
- Why it's built this way - three distinct violation shapes plus one compliant-and-silent control is what proves the rule reads structure (record-ness, readonly-ness, member count) rather than matching on a name pattern; none of that is verifiable by reading the rule's own source, only by compiling both shapes and pointing a map at them, the same discipline CascadeSoftDeleteFitnessTests states explicitly for IL-reading rules.
- Where it's used - an independent class in the Common architecture suite; the fixture wrapper types live in
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/StronglyTypedIdFixtures/StronglyTypedIdFixtures.cs.
TightCeilingTests
MMCA.Common.Architecture.Tests |
MMCA.Common.Architecture.Tests.Cqrs|MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:144| Level 5 | class
- What it is - a private nested fixture inside ConstructorDependencyCountTestsBaseTests: a
ConstructorDependencyCountTestsBasesubclass whose controller and handler ceilings are set tight (2 each) against a nestedFixtureMapthat scans a three-dependency controller and two three-dependency handlers, so both populations are guaranteed to be flagged. - Depends on - ConstructorDependencyCountTestsBase (the abstract base it extends,
ConstructorDependencyCountTestsBaseTests.cs:144), the nestedFixtureMap(:116-:125) pointing both the Api and Application layers at this test assembly, andint.MaxValueforMaxConstructorDependencies, since the Application*Serviceceiling is not exercised by this fixture (:101-:104). - Concept introduced - a ceiling paired with fixtures that deliberately exceed it, the standard shape for proving a counting rule fires. The two ceilings,
MaxControllerConstructorDependencies => 2(:106) andMaxHandlerConstructorDependencies => 2(:108), sit one below the three dependencies the fixture controller and fixture handlers actually declare, soControllers_DoNotExceedConstructorDependencyCeilingandHandlers_DoNotExceedConstructorDependencyCeilingboth throw when run against this map. [Rubric section 14 - Testability] is the technique. - Walkthrough - three overrides only:
Mapreturns the nestedFixtureMap(:100),MaxConstructorDependenciesis left atint.MaxValuewith a comment stating the Application*Servicefact is not exercised here (:102-:104), and the two ceilings (:106,:108) are both set to 2. - Why it's built this way - one map with deliberately-noncompliant fixtures lets ConstructorDependencyCountTestsBaseTests prove the offender-naming behavior (type name plus dependency count) for both the controller and handler populations from a single nested type, rather than duplicating the fixture assembly per population.
- Where it's used - two facts in ConstructorDependencyCountTestsBaseTests:
Controllers_AreFlagged_WhenAControllerExceedsTheCeilingandHandlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling. Private to that file, not used elsewhere.
SubstitutabilityRuleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Substitutability·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/SubstitutabilityRuleTests.cs:16· Level 5 · class (sealed)
- What it is - the meta-test for the Liskov substitutability rule: four facts that pin a refusing override, an unwritten interface implementation, a plain throwing method that must be left alone, and a by-name exclusion.
- Depends on - ArchitectureRules (
SubstitutableMembersDoNotRefuseTheContract,SubstitutabilityRuleTests.cs:25), its nestedFixtureAssemblyMap : ArchitectureMapBase(:73, see ArchitectureMapBase), the fixture types inMMCA.Common.Architecture.Tests.SubstitutabilityFixtures(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/SubstitutabilityFixtures/SubstitutabilityFixtures.cs:9-:53) andXunit.Sdk.XunitException. - Concept introduced - a rule that reads IL cannot be verified by reading it, the same principle taught at CascadeSoftDeleteFitnessTests (class doc,
SubstitutabilityRuleTests.cs:10-:14). The rule (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:50) decides from the IL of each override or interface implementation whether it throwsSystem.NotSupportedExceptionorSystem.NotImplementedException(:12-:15), so the only proof is to compile the offending and innocent members side by side and point a map at them. [Rubric §14 - Testability] is the technique; [Rubric §34 - Architecture Governance & Documentation] is the enforced-not-trusted property. - Walkthrough - every fact calls the rule against a shared
_map(:20) and reads the thrownXunitExceptionmessage.OverrideThrowingNotSupported_IsFlagged(:22-:31): the message namesFixtureRefusingShape.Describeand the exception typeSystem.NotSupportedException, so the report says what was thrown, not only where.InterfaceImplementationThrowingNotImplemented_IsFlagged(:33-:42):FixtureUnfinishedExporter.ExportandSystem.NotImplementedException, the interface-implementation half of the rule.NonOverrideMethodThatThrows_IsNotFlagged(:44-:53):FixtureStandaloneThrower(SubstitutabilityFixtures.cs:40) overrides and implements nothing, so no caller was promised the operation and it must not appear in the message.ExcludedMember_IsNotFlagged(:55-:70): first proves the member IS reported with an empty exclusion set (a control), then excludesFixtureExcludedShape.Cornersby itsNamespace.Type.Methodname (:18) and asserts it vanishes whileFixtureRefusingShapeis still reported, so an exclusion silences only the member it names.
- Why it's built this way - the fixture map is module-less (
Framework(Layer.Domain, typeof(FixtureRefusingShape).Assembly),:79), and a module-less map makes the rule scan every layer (ArchitectureRules.Substitutability.cs:97), which is what lets it reach this test assembly. Each negative case has a positive control beside it, so a rule that silently flagged nothing would fail the first two facts. See ADR-015. - Where it's used - an independent class in the Common architecture suite; the shipped binding of the rule over real code is
SubstitutabilityTestsBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:35).
IntegrationEventContractTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:13· Level 6 · class
- What it is - the meta-test for the frozen integration-event contract base: thirteen facts proving that a reordered member list is not a break, that everything a consumer can actually observe (a missing member, an extra member, a retyped member, a retyped generic argument, a nullability change, a missing event) still is, that the rendered contract is precise enough to see those changes at all, and that the snapshot-regeneration switch writes paste-ready literals.
- Depends on - IntegrationEventContractTestsBase (the type under test, through the nested ProbeTests), ArchitectureRules (
BuildIntegrationEventContract,:199), its nested FixtureMap, the fixture events in the same test assembly (keyed by.FixtureTallyEvent,.FixtureCountEvent,.FixtureNullableLabelEventand the like), and externals xUnit plus AwesomeAssertions. - Concept introduced - a snapshot gate has to be insensitive to everything the wire is insensitive to, and sensitive to everything it is not, or it trains people to update it by rote. The base compares each event's member list as a SET rather than a sequence, and the reason is stated plainly in its own doc (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:10-:16): JSON carries no member order, so reordering two properties in a record declaration changes nothing a consumer can observe, and failing the build for it would teach a team to regenerate the one gate that guards real breakage without reading it. The comparison therefore reports five kinds of difference and no others:MISSING EVENTandNEW EVENT(:69-:73), and per eventMISSING member,changed typeandEXTRA member(:90-:114). The retype is reported as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (:82-:84). The member list is split on commas outside angle brackets (SplitTopLevel,:163-:180), so a multi-argument generic type stays one member, and each line is keyed on the event's[EventName]value when it declares one, its full type name otherwise (:29-:31). [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties; [Rubric §33 - Developer Experience] is the reason for the set comparison, since a gate that cries wolf stops being read. - Walkthrough - the class never hard-codes a whole expected contract. It builds the live one from FixtureMap through
LiveContract()(:198-:199) and then either mutates it or asserts on specific lines of it, so every case stays honest as the fixture types change (class doc,:8-:10).Base_Passes_WhenTheCommittedLiteralsAreAlphabetical(:15-:23): the unmutated baseline, which is the shape every consumer's committed literal is written in today.Base_Passes_WhenMembersAreListedInADifferentOrder(:25-:34): reverses each line's member list throughReverseMembers(:202-:206) and assertsNotThrow, with thebecausestating why order is not observable.Base_Fails_WhenTheCodeDeclaresAMemberTheContractDoesNot(:36-:47) andBase_Fails_WhenTheContractDeclaresAMemberTheCodeDropped(:49-:60): drop the first member and append aGhostProperty:String, asserting"EXTRA member"and"MISSING member GhostProperty:String"respectively.Base_Fails_WhenAMembersTypeChanges(:62-:74): rewrites the first member's type toGuidand asserts"changed type".Base_Fails_WhenAnEventIsMissingFromTheSnapshot(:76-:83): drops the first line entirely and asserts"NEW EVENT", which is the direction that matters most, an event shipped without a consumer rollout.Contract_SpellsOutGenericArguments_AndPinsAnIntermediateBase(:85-:98): asserts the exactFixtureShapedEventline,{ Count:Nullable<Int32>, Origin:String, Tags:IReadOnlyList<String> }, so generic arguments and a member inherited from an intermediate consumer base are both rendered, and asserts no fixture line carriesSchemaVersion:orDateOccurred:, because the framework envelope is the framework's contract, not the event's.Base_Fails_WhenAGenericArgumentOfAMultiArgumentMemberChanges(:100-:114): rewrites<String, Int32>to<String, Int64>on theFixtureTallyEventline and asserts"member Counts changed type", proving the comma inside the angle brackets belongs to the member type and not to the member list.Contract_Differs_WhenTheOnlyChangeIsIntToNullableInt(:116-:124): the member blocks ofFixtureNullableCountEventandFixtureCountEventmust differ, sinceinttoint?changes what a consumer must accept.Contract_Differs_WhenTheOnlyChangeIsANullableReferenceAnnotation(:126-:137): pins{ Label:String?, Notes:IReadOnlyList<String?> }against its non-nullable twin{ Label:String, Notes:IReadOnlyList<String> }, so both a nullable member and a nullable generic argument are part of the rendered promise.Base_Fails_WhenOnlyAMembersNullabilityChanges(:139-:153): strips the?fromLabel:String?and asserts"member Label changed type".Contract_KeysAnEventOnItsEventName_WhenItDeclaresOne(:155-:166): the contract containsFixture.Named.v1 { Value:Int32 }and no line mentionsFixtureNamedEvent, so a CLR rename does not churn the snapshot of an event whose[EventName]is the wire identity the outbox stores.Base_WritesTheLiveContractAsLiterals_WhenTheSnapshotOutputVariableIsSet(:168-:188): setsIntegrationEventContractTestsBase.SnapshotOutputVariable(MMCA_CONTRACT_SNAPSHOT_OUT, base:27) to a temp path, runs the probe fact, and asserts the file holds each live line as a quoted, comma-terminated C# literal (the base writes it at:40-:44); afinallyclears the variable and deletes the file.MutateFirstMultiMemberEvent(:212-:221) is the helper that keeps the four generic failure cases robust: it finds the first event declaring more than one member and asserts that such an event exists (:216), so a fixture reshaped down to single-member events fails loudly instead of making the cases vacuous.Split(:223-:228) parses one line back into a name and a member array throughSplitTopLevel(:231-:248), which mirrors the base's angle-bracket-aware splitter;MembersOf(:191-:195) returns the member block of the single line containing a marker.
- Why it's built this way - deriving the expectations from the live contract and mutating them is what stops this meta-test from becoming a second snapshot that also needs maintaining. The few literal lines it does pin are the rendering rules themselves (generic arguments, nullability, the
[EventName]key), where the exact text is the point. The probe subclasses areprivateso xUnit does not collect their inherited facts as tests of their own (class doc,:10-:11), the same discipline DriftedTests follows. The snapshot-output fact mutates a process-wide environment variable; its own comment (:171-:172) relies on it being the only fact that invokes the probe this way and on xUnit running one class's tests sequentially. - Where it's used - an independent class in the Common architecture suite. The base it protects is subclassed per repo by the consumers, whose committed
ExpectedContractliterals are the real subject of the gate. - Caveats / not-in-source - the base's
Parsekeeps a malformed committed line, braces stripped, as an event with no members (IntegrationEventContractTestsBase.cs:131-:138) so it surfaces as a mismatch rather than being silently dropped. No fact here exercises that path.
LocalizationResourceTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12· Level 6 · class
- What it is - the MMCA.Common binding of the resource-completeness rule: it asserts the framework's own
.resxfiles fully translate every supported non-default culture, deriving the required-culture set from the live allowlist rather than restating it. - Depends on - LocalizationResourceTestsBase (base rule,
LocalizationResourceTests.cs:12) and SupportedCultures (source of the required cultures,:15). - Concept introduced - deriving a gate's expectations from the same allowlist production uses. Instead of hardcoding "translate Spanish," the override computes
RequiredCulturesasSupportedCultures.AllminusSupportedCultures.Default(:14-:17), so adding a locale to the app automatically extends the coverage requirement. [Rubric §27 - i18n] assesses whether localization is complete and enforced; this gate makes a missing translation a build failure and self-updates when the supported set grows. - Walkthrough - two members.
RequiredCultures(:14-:17) filtersSupportedCultures.Allto the non-default entries; the allowlist is[Default, "es"]withDefault = "en-US"(MMCA.Common/Source/Core/MMCA.Common.Shared/Globalization/SupportedCultures.cs:12,:18), so the required set is today exactlyes.MinimumBaseResources => 3(:21) sets a non-vacuous floor: the scan must find at least three base resources (ErrorResources for the API, plus SharedResource and MudTranslations for the UI), so a wrong scan root or repo re-layout cannot let the gate pass having checked nothing. The base's default for that floor is zero, which skips the guard entirely (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:21), so overriding it here is what makes the gate honest. The single inherited[Fact]isTranslations_AreComplete_ForEveryRequiredCulture(LocalizationResourceTestsBase.cs:23-:25). - Why it's built this way - ADR-027 (localization) requires supported cultures to be fully translated; the derived allowlist plus the minimum-count floor turn that into a self-maintaining CI gate. The pseudo-locale
qps-Plocis deliberately kept out ofSupportedCultures.All(SupportedCultures.cs:20-:28) precisely so this gate does not demand a.qps-Ploc.resxsibling. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite.
ModuleProbeMap
MMCA.Common.Architecture.Tests.Contracts ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:87· Level 6 · class
- What it is - the module-bearing twin of FrameworkProbeMap: the identical test assembly, declared under one
"Probe"module's Shared layer instead of a framework layer, so the residency rule has a module boundary to check against. - Depends on -
ArchitectureMapBase,LayerandLayerRef(theModule()helper), and FixtureLeakingEvent as the assembly anchor. - Concept - pairing this map with FrameworkProbeMap over the SAME assembly changes exactly one variable, module presence, so
ResidencyRule_FlagsAnEventOutsideASharedAssembly(IntegrationEventPayloadPurityTests.cs:30-:39) can prove the rule reacts to the module boundary specifically and not to anything else about the assembly's shape. - Walkthrough -
RepoToken => "MMCA.Common"(:89) and a singleModule("Probe", Layer.Shared, typeof(FixtureLeakingEvent).Assembly)layer (:91-:92). The test assembly's real name does not end in.Shared, so declaring it Shared under a module is what gives the residency rule something to flag. - Why it's built this way - the class doc above
IntegrationEventPayloadPurityRuleTestsstates the intent directly (:23-:26): "a rule that only ever passes proves nothing", both rules are pointed at this assembly whose fixtures include a deliberately bad event, and both must name it.FrameworkProbeMapandModuleProbeMapare the two probes that make that adversarial coverage possible without touching the rule under test. - Where it's used -
ResidencyRule_FlagsAnEventOutsideASharedAssembly(:30-:39), asserting the message contains FixtureCleanEvent and "is not a *.Shared assembly". Private toIntegrationEventPayloadPurityRuleTests; not used outside this file.
NavigatingQuerySpec, ScalarOnlyQuerySpec
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain· (see per-type table) · Level 6 · class
The same unsafe and safe pair as NavigatingSpec and ScalarOnlySpec, rebuilt one level further down the specification hierarchy as QuerySpecification<TEntity, TIdentifierType> subclasses. They exist to pin a single ruling: adding includes, ordering, and paging to a specification must not take it out of the navigation rule's reach.
- Depends on - QuerySpecification<TEntity, TIdentifierType> (both,
SpecificationFitnessTests.cs:76,:89), FitnessDependent and FitnessPrincipal (the entities), andSystem.Linq.Expressions(:1). - Concept introduced - walking the whole base chain instead of matching one type. The rule selects candidates with
HasBaseTypeStartingWith("MMCA.Common.Domain.Specifications.Specification")(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:7,:33), and that helper climbsBaseTypeall the way up (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:88-:91). BecauseQuerySpecificationitself derives fromSpecification, the prefix still matches two levels down, and the string prefix match means bothSpecificationandQuerySpecificationare caught by the one condition. That is the kind of thing that is true today and could quietly stop being true after a refactor of the specification hierarchy, which is exactly why it gets its own fact rather than being left as an assumption. [Rubric §7 - Microservices Readiness] and [Rubric §8 - Data Architecture] are the underlying properties (a navigating predicate cannot cross a data-source boundary, ADR-006); [Rubric §14 - Testability] is why the pair exists. - Walkthrough - both declare an explicit parameterless constructor, which is load-bearing: the rule instantiates candidates to read
Criteriaand skips any type without one (ArchitectureRules.Specifications.cs:37-:41). Each constructor also exercises a different part of the query surface, so the fixtures prove that the extra machinery does not interfere with the analysis.
| Type | File:Line | What differs |
|---|---|---|
NavigatingQuerySpec |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:76 |
The offender. Its constructor adds ordering and paging, AddOrderBy(d => d.PrincipalId) and ApplyPaging(skip: 0, take: 10) (:79-:83), and its Criteria dereferences the navigation, d => d.Principal!.IsActive (:86). Must be reported. |
ScalarOnlyQuerySpec |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:89 |
The safe counterpart, and a deliberately awkward one: its constructor calls AddInclude(nameof(FitnessDependent.Principal)) (:92), so it does pull in the related entity, while its Criteria filters only on own columns (:95). Must not be reported. Includes are an eager-loading instruction, not a filter predicate, and the rule analyzes the predicate only. |
- Why they're built this way -
ScalarOnlyQuerySpecis the sharper of the two. Without it, a future rule implementation that (reasonably but wrongly) also inspectedIncludeswould still pass every other fixture in the file. Both are declaredpublicwhile their plain-Specificationsiblings areprivate, which makes no difference to the rule (it scansConcreteClassesacross the assembly, not by visibility). - Where they're used - the two inputs to
Rule_AlsoAnalyzesQuerySpecificationsin SpecificationFitnessTests (:26-:38).
LifetimeTokenConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui.ComponentConventions·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/LifetimeTokenConventionTests.cs:12· Level 6 · class (sealed)
- What it is - Common's binding of the lifetime-token gate: no component code reads
_cts.Tokendirectly, it goes through aLifetimeTokenproperty, plus a detector theory and two guards proving the scan cannot pass vacuously. - Depends on -
LifetimeTokenConventionTestsBase(LifetimeTokenConventionTests.cs:12; base atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13), ArchitectureMapBase.FindRepoRoot(:14-:15), and ArchitectureRules (ComponentsReadTheirTokenThroughLifetimeToken,ArchitectureRules.LifetimeTokens.cs:28;DirectTokenReads,:75). - Concept introduced, reading
Tokenoff a disposed source is the hazard. The base doc (LifetimeTokenConventionTestsBase.cs:6) names it: a component that reads_cts.Tokenafter its source is disposed throws, so reads go throughLifetimeToken, which hands back an already-canceled token instead. The inherited factComponents_ReadTheirTokenOnlyThroughLifetimeToken(LifetimeTokenConventionTestsBase.cs:27-:29) runs the rule overScanRootwithMinimumCodeBehindFilesandScanAllCodeFiles. [Rubric §34 - Architecture Governance & Documentation] covers the executable convention. - Walkthrough - three overrides bind it:
ScanRootisSource/Presentation/MMCA.Common.UI(:14-:15),MinimumCodeBehindFiles => 15(:17, a floor; 17.razor.csfiles when the gate landed,:16), andScanAllCodeFiles => true(:19) so plain.csfiles are scanned too (the base default isfalse,LifetimeTokenConventionTestsBase.cs:25). Three members then test the rule itself:Detector_FlagsADirectReadOutsideTheLifetimeTokenProperty(:74-:83): a seven-row theory overDirectTokenReads;_cts.Tokenand_cts!.Tokenare flagged, theLifetimeTokenproperty bodies (both the plain and the null-guarded form), a different field name and_cts.LifetimeToken()are not.DirectRead_IsReportedWithFileLineAndTheFix(:85-:88): the report readsPages/Sample.razor.cs:2 reads _cts.Token directly; use LifetimeToken, so the message is the instruction.TooFewCodeBehindFiles_FailsRatherThanPassingVacuously(:90-:96) andMissingRoot_FailsRatherThanScanningNothing(:98-:104): a floor of 10,000 throws "expected at least 10000" (ArchitectureRules.LifetimeTokens.cs:51), and a nonexistent folder throws "scan root not found" (:36).
- Where it's used - MMCA.ADC binds the same base (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs).
FixtureMap
MMCA.Common.Architecture.Tests |
MMCA.Common.Architecture.Tests.GovernanceandMMCA.Common.Architecture.Tests.Cqrs| (see per-type table) | Level 4 | class
The name is reused three times in this assembly, one per outer test class that needs its own narrowly-scoped map; two are in this group's scope and documented here, and a third, nested inside IntegrationEventContractTestsBaseTests (Contracts namespace), scopes the integration-event contract rule instead. All three are private sealed, nested in different outer classes, which is what keeps them from colliding.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept - cross-references the module-versus-framework distinction taught at FixtureModuleMap. The Governance instance is built with
Framework(...)(FeatureFlagLifecycleTests.cs:108-:109), because the rules it feeds,FeatureFlagsDeclareLifetimeandTemporaryFeatureFlagsAreNotPastRemoveBy, read whatever assemblies the map lists without a module-scoping distinction. The Cqrs instance is built with twoModule("Fixtures", ...)entries instead, one per scanned layer (ConstructorDependencyCountTestsBaseTests.cs:126-:83), becauseConstructorDependencyCountTestsBase's ceiling check is layer-specific: API controllers offMap.Api(), CQRS handlers offMap.ModuleApplication(). - Walkthrough - both declare
RepoToken => "MMCA.Common"(FeatureFlagLifecycleTests.cs:106;ConstructorDependencyCountTestsBaseTests.cs:123). The Governance instance'sDefineLayers()returns a singleFramework(Layer.Shared, typeof(FixtureBadFeatures).Assembly)(:108-:109). The Cqrs instance'sDefineLayers()returns two entries,Module("Fixtures", Layer.Api, typeof(FatFixtureController).Assembly)andModule("Fixtures", Layer.Application, typeof(FatFixtureController).Assembly)(ConstructorDependencyCountTestsBaseTests.cs:126-:83), so the same fixture assembly, anchored onFatFixtureController, answers both the API and Application scans the ceiling rule runs.
| Type | File:Line | The rule it scopes |
|---|---|---|
FixtureMap (in FeatureFlagLifecycleRuleTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:104 |
Constructed fresh at each call site: the private DeclarationFailure() helper for ArchitectureRules.FeatureFlagsDeclareLifetime (:98), and directly by the two ExpiryRule_* facts for ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy (:72,:87). |
FixtureMap (in ConstructorDependencyCountTestsBaseTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:121 |
Both Map-defining fixture subclasses point at a fresh FixtureMap(): TightCeilingTests (:100), whose tight ceilings make the offender assertions in Controllers_AreFlagged_WhenAControllerExceedsTheCeiling and Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling fire, and ConformantTests (:112), whose wider ceilings back the two *_AreNotFlagged facts. |
- Why it's built this way - each is nested
private sealedinside the test class that uses it, so neither is visible to the other and neither can be mistaken for a repo map. Duplicating a handful of lines per outer class is cheaper than a shared fixture map whose meaning would have to be read from another file. - Where they're used - see the table above; each instance is constructed only inside the outer test class that declares it.
ConstructorDependencyCountTestsBaseTests
MMCA.Common.Architecture.Tests |
MMCA.Common.Architecture.Tests.Cqrs|MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencyCountTestsBaseTests.cs:14| Level 6 | class
- What it is - the meta-test for
ConstructorDependencyCountTestsBase: nine facts proving the controller and handler constructor-dependency ceilings each flag an offender by name and dependency count, both pass a fixture sitting exactly on the ceiling, both fail against a map whose layers hold no controller or handler at all, and the companion tightness facts flag a ceiling set above the observed maximum while accepting one set exactly on it. - Depends on - ConstructorDependencyCountTestsBase (
Controllers_DoNotExceedConstructorDependencyCeiling,Handlers_DoNotExceedConstructorDependencyCeiling), its own nested TightCeilingTests,ConformantTestsandEmptyScanTestssubclasses (:139-:171), the nestedFixtureMapandBareMap(:116-:137), and the fixture population inMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs(FatFixtureController,RebuildFixtureProjectionHandler,GetFixtureProjectionHandler). - Concept introduced - a ceiling is a high-water mark, not a strict less-than.
ConformantTestssets both ceilings to 3, exactly the dependency count the fixture controller and fixture handlers declare, andControllers_AtTheCeiling_AreNotFlagged/Handlers_AtTheCeiling_AreNotFlaggedassertNotThrowagainst it (:68-:75,:92-:99), proving the rule accepts equality rather than only strictly-fewer. [Rubric section 15 - Best Practices & Code Quality] is the property; [Rubric section 14 - Testability] is the technique, an IL/reflection-driven count that only compiling both a tight and an exact-fit fixture population can prove either way. - Walkthrough - all five facts build a subclass instance and read its two assertion delegates.
Controllers_AreFlagged_WhenAControllerExceedsTheCeiling(:57-:66): against TightCeilingTests, asserts the thrown message namesFatFixtureControllerwith"(3 ctor dependencies)"(:63-:65).Controllers_AtTheCeiling_AreNotFlagged(:68-:75): againstConformantTests, assertsNotThrow.Handlers_AreFlagged_WhenEitherHandlerContractExceedsTheCeiling(:77-:90): asserts the message names bothRebuildFixtureProjectionHandlerandGetFixtureProjectionHandler, each with"(3 ctor dependencies)", provingICommandHandlerandIQueryHandlerimplementations are scanned as one population (:85-:89).Handlers_AtTheCeiling_AreNotFlagged(:92-:99): the handler-side equality control.BothPopulations_FailVacuousScans(:101-:113): againstEmptyScanTests(MapisBareMap, pointing both layers atCommon.Shared.Abstractions.Result's assembly, which holds neither a controller nor a handler), asserts BOTH assertion delegates throw, one because the API assembly holds no controller to check and one because the Application assembly holds no handler, the non-vacuity floor for this rule.LooseControllerCeiling_IsFlagged_NamingPopulationCeilingAndObservedMax(:74-:88): against LooseCeilingTests (ceiling 4, widest controller takes 3), assertsControllers_ConstructorDependencyCeilingIsTightthrows a message naming the population ("API controllers"), the ceiling property and value ("MaxControllerConstructorDependencies is 4"), the observed maximum ("takes 3"), theFatFixtureControllerat that maximum, and the fix ("from 4 to 3").LooseHandlerCeiling_IsFlagged(:90-:97): the handler-side check, the message names "CQRS handlers".CeilingsAtTheObservedMaximum_AreTight(:99-:109): againstConformantTests, both tightness factsNotThrow.TightnessCheck_FailsVacuousScans(:111-:118): againstEmptyScanTests, a population with nothing to measure cannot prove its ceiling tight, so the fact throws.FixtureMap(:121-:130) andBareMap(:133-:142): private nestedArchitectureMapBases,RepoToken => "MMCA.Common", each declaring one Api-layer and one Application-layerModuleentry over this test assembly (FixtureMap) or overCommon.Shared.Abstractions.Result's assembly (BareMap).
- Why it's built this way - three subclasses (
TightCeilingTests,ConformantTests,EmptyScanTests) sharing one fixture map versus a bare one is what proves the ceiling comparison accepts equality but not excess, and that a scan finding no controller or handler still fails loudly rather than passing by omission. - Where it's used - an independent class in the Common architecture suite; its usage site is the fixture population file
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/ConstructorDependencyFixtures/ConstructorDependencyFixtures.cs.
DependencyVersionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9· Level 6 · class
- What it is - the MMCA.Common binding of the shared dependency-pin rule: a one-line sealed subclass that turns on the checks enforcing this repo's two commercial-license package ceilings, MassTransit below major 9 and SixLabors.ImageSharp below major 4.
- Depends on - DependencyVersionTestsBase (
public sealed class DependencyVersionTests : DependencyVersionTestsBase;,DependencyVersionTests.cs:9), which in turn callsArchitectureRules.PinnedPackageMajorBelowagainst this repo'sDirectory.Packages.props. - Concept introduced - the thin-subclass fitness pattern. Almost every rule in this project lives once in the reusable
MMCA.Common.Testing.Architecturepackage as an abstract*TestsBase, and each repo activates it with a near-empty subclass. This is the first of many such subclasses; the body-less form here is the extreme case (no configuration at all: the declaration ends at its semicolon,DependencyVersionTests.cs:9). [Rubric §32 - Dependency & Supply-Chain] assesses guarding against risky dependency drift; the base parses the pinned versions and fails the build before either ceiling is crossed, so a "just bump the version" edit cannot slip through. - Walkthrough - no members; the entire behavior is inherited. The base contributes two
[Fact]s.MassTransit_MustNotExceed_MajorVersion8(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:27-:37) walks the three MassTransit package idsMassTransit,MassTransit.RabbitMQ, andMassTransit.Azure.ServiceBus.Core(:17-:22) with anexclusiveMajorCeilingof 9, because v9 requires a commercial license (MT_LICENSE) and every broker-enabled host fails its startup license check without one, while CI never starts a broker so the build would otherwise stay green (:32-:35).ImageSharp_MustNotExceed_MajorVersion3(:47-:60) does the same forSixLabors.ImageSharp(:45) with a ceiling of 4, because ImageSharp v4's MSBuild targets fail at build time without a$(SixLaborsLicenseKey)(:55-:58). Both id lists areprotected virtual, so a repo that pins neither package can override them to an empty list and make the rule vacuous. - Why it's built this way - the pins are real only in MMCA.Common (where these packages are actually declared); ADC and Store inherit MassTransit transitively through
MMCA.Common.Infrastructureand deliberately do not subclass the base, because the default list would assert aDirectory.Packages.propsentry they do not declare (DependencyVersionTestsBase.cs:8-:13). See ADR-016, which records the lockstep-versioning policy and the license-ceiling pattern these two facts implement. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob. - Caveats / not-in-source - the class's own doc comment (
DependencyVersionTests.cs:5-:8) still describes only the MassTransit trap; the ImageSharp ceiling arrived later on the base and the subclass doc was not extended. The behavior is the base's two facts, not the comment.
FixtureMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:254· Level 6 · class
- What it is - a consumer-shaped map over this test assembly, declaring one module Shared layer, so the integration-event contract scan covers the fixture events here and none of the framework's own.
- Depends on - ArchitectureMapBase (
private sealed class FixtureMap : ArchitectureMapBase,IntegrationEventContractTestsBaseTests.cs:254), LayerRef and the Layer enum. - Concept - cross-references the module-versus-framework distinction taught at FixtureModuleMap and FakeConsumerMap. The single
Module("Fixture", Layer.Shared, ...)entry (:130) does two things at once. It makes the map module-bearing, soIntegrationEventsexcludes framework layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:157-:73) and no framework-shipped event can leak into the contract the probes compare; and it names Shared, which is where a consumer's integration events are required to live, so the fixture events do not simultaneously trip the residency rule.RepoToken => "MMCA.FixtureRepo"(:126) is deliberately unlikeMMCA.Common, so nothing derived from the token can collide with the real framework namespaces. - Walkthrough -
DefineLayers()is ayield-based iterator with one entry (:128-:131). The events it brings into scope are the eight upcaster fixture contracts, which are the onlyIIntegrationEventimplementations this assembly declares. That reuse is what makes the class doc's promise hold, that the mutation cases stay honest as the fixture types change (:9-:10): the baseline is rebuilt from live types on every call rather than hard-coded. - Where it's used - built fresh by
LiveContract()(:86-:87) on every mutation, and supplied as theMapof every ProbeTests instance (:136).
FolderWidthTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:16· Level 6 · class
- What it is - the MMCA.Common binding of the folder-width rule: a sealed subclass that points the shared rule at this repo's root and declares the three folders this repo keeps deliberately flat.
- Depends on - FolderWidthTestsBase (
public sealed class FolderWidthTests : FolderWidthTestsBase,FolderWidthTests.cs:16) and ArchitectureMapBase forFindRepoRoot("MMCA.Common.slnx")(FolderWidthTests.cs:18), which walks upward until it finds the solution file so the test does not depend on the working directory. - Concept - the same thin-subclass fitness pattern taught at DependencyVersionTests, here with configuration rather than none: the repo supplies
RepoRootand, because it has documented exceptions,ExemptFolderSuffixes. [Rubric §5 - Modularity & Structure] is the property. The rule enforces "feature by folder, use case by leaf": no folder underSource/orTests/may hold more thanMaxDirectFilesdirect code units, defaulting to 12 (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:23), so a folder keeps naming a feature instead of drifting into a technical bucket (base doc,FolderWidthTestsBase.cs:3-:13). - Walkthrough - two overrides and no facts of its own.
RepoRoot(FolderWidthTests.cs:18) is an initialized get-only property, so the upward search forMMCA.Common.slnxruns once per test instance.ExemptFolderSuffixes(FolderWidthTests.cs:26-:33) returns three repo-relative suffixes:MMCA.Common.Application/UseCases/Decorators,MMCA.Common.Application.Tests/Decorators, andMMCA.Common.Domain/Interfaces. The doc above it (:22-:27) gives the reason for the first pair: the decorator pipeline is nine cross-cutting concerns times command and query, so splitting it by concern would yield nine two-file folders, and the test twin mirrors it. The third is the entity marker interfaces.- The single inherited
[Fact],Folders_stay_narrow(FolderWidthTestsBase.cs:31-:33), delegates toArchitectureRules.FoldersStayNarrow(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34), which walksSourceandTestsrecursively (:42-:50), skips the tool-owned and build-output segmentsbin,obj,Migrations,Platforms,Resources,node_modules,wwwrootand.git(:82), and counts a.razorcomponent and its co-locatedX.razor.cscode-behind as one unit (:105-:119).
- Why it's built this way - counting code units rather than files is what keeps the cap honest for a Blazor project, where a component is three files that describe one thing; skipping
Migrations/andPlatforms/is what keeps generated and SDK-owned trees from failing a rule about human-authored layout. Exemptions are folder-path suffixes rather than a blanket switch, so each accepted flat namespace is a line someone had to write and can be read back. See ADR-109 for the convention itself and ADR-015 for the fitness-function approach. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI. The same base is activated once per repo; the ADC binding is the identical shape withFindRepoRoot("MMCA.ADC.slnx")and no exemptions at all (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-:12). - Caveats / not-in-source - the class-level doc comment (
FolderWidthTests.cs:5-:17) describes a wider exemption set than the class configures, naming the application contracts, the shared auth contracts, the API startup extensions and the integration-test package root. The behavior is the three suffixes actually listed at:30-:32; the property's own doc (:22-:27) records the current rationale and points atUPGRADING.md.
CascadeSoftDeleteFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17· Level 6 · class
- What it is - the meta-test for the cascade-soft-delete rule: six facts that pin both accepted cascade forms, both violation reasons with their named child collection, the out-of-scope aggregate, and both granularities of the exemption list.
- Depends on - ArchitectureRules (
AggregatesCascadeSoftDeleteToChildren,CascadeSoftDeleteFitnessTests.cs:26), its nested FixtureAssemblyMap, the seven types inMMCA.Common.Architecture.Tests.CascadeFixtures(:1), andXunit.Sdk.XunitException(:3). - Concept introduced - a rule that reads IL cannot be verified by reading it. The class doc states the principle in those words (
:7-:16), and it is the organizing idea behind this whole family of self-tests.AggregatesCascadeSoftDeleteToChildrendecides its verdict from the instruction stream of aDelete()override, so no amount of reviewing the rule's source proves it distinguishesbase.Delete()fromchild.Delete(). The only proof is to compile both shapes side by side and point a map at them. [Rubric §14 - Testability] is the technique; [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties, taught at CascadeChildFixture. - Walkthrough - a
private const string FixtureNamespace(:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact, and five of the six read the message of a rule call that always throws.AggregateWithoutDeleteOverride_IsFlagged_WithItsChildCollection(:23-:33): asserts three things about one line, the type name, the reason"no Delete() override", and the collection name"_orphans"(:30-:32). Pinning the collection name is what makes the failure actionable rather than merely true.DeleteOverrideThatOnlyDeletesItself_IsFlagged_WithItsChildCollection(:35-:45): the same three assertions for the other reason,"Delete() never deletes a child"and"_ignored". The two distinct reason strings are the rule distinguishing "you forgot" from "you tried and it does not work".BothCascadeForms_AreAccepted(:47-:60): asserts both the helper form and the hand-rolled loop are absent, each with abecausenaming the form.AggregateWithNoChildEntities_IsIgnored(:62-:71): the scope fact, with the ruling stated inline, aList<string>is not a child-entity collection.AllowlistedType_SilencesOnlyThatType(:73-:85) andAllowlistedNamespace_SilencesTheRule(:87-:94): the two exemption granularities. The type-level fact carries the control assertion, that the other offender is still reported (:83-:84).
- Why it's built this way - two structurally identical offenders is what makes the type-level exemption fact meaningful; two distinct violation reasons is what keeps a regression in the body reader from hiding behind the simpler missing-override check. See ADR-005 and ADR-015.
- Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is CascadeSoftDeleteConventionTestsBase, activated here by CascadeSoftDeleteConventionTests and in both consumer repos over their own modules.
FrameworkProbeMap
MMCA.Common.Architecture.Tests.Contracts ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:78· Level 6 · class
- What it is - a private nested
ArchitectureMapBaseover the payload-purity test assembly with no module boundary declared, one of a matched pair with ModuleProbeMap that isolates whether a rule's behavior comes from module presence or from the assembly itself. - Depends on -
ArchitectureMapBase,LayerandLayerRef(theFramework()helper), and FixtureLeakingEvent (only as the assembly anchortypeof(FixtureLeakingEvent).Assembly). - Concept introduced - a moduleless map answers to a different baseline.
IntegrationEventsLiveInSharedAssembliesonly checks residency for module-bearing maps, so pointing it atFrameworkProbeMapis deliberately vacuous:ResidencyRule_IsVacuousForAModuleLessMap(IntegrationEventPayloadPurityTests.cs:41-:46) assertsNotThrow, "a framework map has no module boundary, so its events answer to its public API baseline rather than to a module's Shared assembly". The same map, unchanged, then IS used to prove the payload-purity rule (a check that does not care about modules), which is what keeps the two rules' scopes distinct. [Rubric §7 - Microservices Readiness] is the property the residency rule defends; [Rubric §30 - Compliance/Privacy/Data Governance] is what the payload-purity rule defends, since a domain type reached through a nested payload is exactly how a PII field leaks onto the wire. - Walkthrough -
RepoToken => "MMCA.Common"(:96) and a singleFramework(Layer.Shared, typeof(FixtureLeakingEvent).Assembly)layer (:98-:99); no module entry. - Where it's used - three facts in
IntegrationEventPayloadPurityRuleTests:ResidencyRule_IsVacuousForAModuleLessMap(:41-:46),PayloadRule_FlagsADomainTypeReachedThroughANestedPayload(:48-:59, asserting the message names the pathFixtureLeakingEvent.Payload.Entityand the leaking assemblyMMCA.Common.Domain), andPayloadRule_LeavesACleanEventAlone(:61-:70, asserting FixtureCleanEvent is absent from the failure). Private to that class; not used outside this file.
InlineStyleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui.ComponentConventions·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleTests.cs:12· Level 6 · class (sealed)
- What it is - Common's binding of the no-inline-style gate: a four-line subclass that points the shared base at this repo's
Source/tree and sets the floor on how many.razorfiles the scan must reach. - Depends on -
InlineStyleTestsBase(class InlineStyleTests : InlineStyleTestsBase,InlineStyleTests.cs:12; base atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12), ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx")(:14), and ArchitectureRules. - Concept introduced, a base-class binding supplies only the repo-specific facts. The inherited fact
RazorMarkup_CarriesNoInlineStyles(InlineStyleTestsBase.cs:29-:31) callsArchitectureRules.RazorMarkupCarriesNoInlineStyles(MarkupRoots, MinimumRazorFiles, AllowedInlineStyles)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37). The subclass overrides only the two abstract members,RepoRootandMinimumRazorFiles;MarkupRootsdefaults toSourceunder the repo root (InlineStyleTestsBase.cs:21) andAllowedInlineStylesto empty (:27), so Common carries no reviewed exemption. [Rubric §20 - Design System & Theming] is the property protected (styling lives in stylesheets, not markup), and [Rubric §34 - Architecture Governance & Documentation] the enforced-not-trusted half. - Walkthrough -
RepoRoot(:14) resolves the repo fromMMCA.Common.slnx.MinimumRazorFiles => 50(:18) is a floor, not an equality: the comment records 56.razorfiles when the gate landed (53 in UI, 2 in UI.Web, 1 in UI.Maui), so fewer than 50 means the scan root moved, and the rule fails with "expected at least" instead of passing on an empty scan (ArchitectureRules.InlineStyles.cs:68). - Where it's used - the sibling in this repo is
InlineStyleFitnessTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleFitnessTests.cs:12), and MMCA.ADC binds the same base with its own floor (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs).
FixtureAssemblyMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs.Repositories· (see per-type table) · Level 9 · class
The name is used five times in this assembly, once per IL-reading rule under proof, and all five are the same one-entry map: this test assembly registered as a single framework layer, so the rule's whole-layer scan lands on the fixtures. They are nested in different outer classes, so their full names differ and none is visible outside its own file.
- Depends on - ArchitectureMapBase, LayerRef and the Layer enum.
- Concept introduced - the layer a fixture map declares is not decoration, it is the scope selector. Each of these five rules starts from a different part of the map, so each map has to name the matching layer or the rule reads nothing and passes vacuously. The cascade and hard-delete rules walk every assembly the map registers, through
ScannableAssemblyLocations(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88-:92), so their layer choice is nominal; the domain-throw rule readsmap.OfLayer(Layer.Domain)and nothing else (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:111-:111), so registering Domain is mandatory there, and the read-repository rule readsmap.OfLayer(Layer.Application)only (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:86-:89), so Application is mandatory there. All five useFramework(...)rather thanModule(...), which is the opposite choice from FixtureModuleMap and correct for the same reason: these rules do not filter on module ownership, so the simplest entry is the honest one. Reading assemblies off disk byAssembly.Locationalso explains a shared limitation: a rule in this family cannot inspect a dynamic or single-file assembly, and the helper silently skips any location that does not exist on disk. [Rubric §14 - Testability] assesses whether a guardrail is provable with focused inputs, and a map that scopes the scan to fixtures is what makes each of these five rules provable at all. - Walkthrough - each declares
RepoToken => "MMCA.Common"and a single-entryDefineLayers()naming one layer and one anchor type from the fixture namespace it serves. Anchoring bytypeof(SomeFixture).Assemblyrather than byAssembly.GetExecutingAssembly()is deliberate: it ties the map to a type a compiler would fail on if the fixtures moved.
| Type | File:Line | Layer and anchor | The rule it scopes |
|---|---|---|---|
FixtureAssemblyMap (in CascadeSoftDeleteFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:97 |
Framework(Layer.Domain, typeof(CascadeChildFixture).Assembly) (:103) |
AggregatesCascadeSoftDeleteToChildren. |
FixtureAssemblyMap (in SoftDeleteEnforcementFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:77 |
Framework(Layer.Infrastructure, typeof(FixtureEntity).Assembly) (:83) |
HardDeletesOnlyInAllowedTypes. |
FixtureAssemblyMap (in DomainEventHandlerSaveFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:110 |
Framework(Layer.Application, typeof(FixtureDomainEvent).Assembly) (:116) |
DomainEventHandlersDoNotSave. |
FixtureAssemblyMap (in DomainThrowFitnessTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:150 |
Framework(Layer.Domain, typeof(NonThrowingFixture).Assembly) (:144) |
DomainThrowsOnlyArgumentGuards, which reads the Domain layer only, so this entry is load-bearing. |
FixtureAssemblyMap (in QueryHandlerReadRepositoryTests) |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:58 |
Framework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly) (:64) |
QueryHandlersUseReadRepositories, which reads the Application layer only. |
- Why they're built this way - five nested six-line classes rather than one shared fixture map, so each test file is readable end to end and no test can change another's scope by editing a shared file. It is the same trade-off FixtureModuleMap makes.
- Caveats / not-in-source - because each map registers the whole test assembly, a rule in this family sees every type in it, not just the fixtures in its own namespace. Four of the five are unaffected because their fixtures are the only matching shapes present; the domain-throw rule is not, which is why it carries an explicit allowlist of the assembly's non-fixture throws (
DomainThrowFitnessTests.cs:29-:33). - Where they're used - constructed once as a readonly field per test class (
CascadeSoftDeleteFitnessTests.cs:21,SoftDeleteEnforcementFitnessTests.cs:19,DomainEventHandlerSaveFitnessTests.cs:21,DomainThrowFitnessTests.cs:36,QueryHandlerReadRepositoryTests.cs:18) and passed to every rule call in each.
ProbeTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventContractTestsBaseTests.cs:264· Level 6 · class
- What it is - a parameterized negative fixture: a
private sealedsubclass of the frozen-contract base whoseExpectedContractis whatever the enclosing test hands its constructor. - Depends on - IntegrationEventContractTestsBase (
private sealed class ProbeTests(IReadOnlyList<string> expected) : IntegrationEventContractTestsBase,IntegrationEventContractTestsBaseTests.cs:264) and FixtureMap (:136). - Concept introduced - parameterizing the drift instead of hard-coding it. DriftedTests is the same technique with fixed wrong values, which works when a base has three assertions and each needs one wrong constant. Here the base has one assertion with six interesting failure modes, so a fixed drifted subclass would mean six near-identical nested classes. A primary-constructor parameter collapses them into one type instantiated six times with different data (
:20,:30,:41,:54,:68,:79). The trade-off is worth naming: the parameterized form is only safe because the expected contract is derived from the live one and then mutated, so a probe can never assert against a stale hand-written literal. - Walkthrough - two overrides, both initialized rather than computed:
Map { get; } = new FixtureMap()(:136) andExpectedContract { get; } = expected(:138). Beingprivatekeeps xUnit from collecting the inheritedIntegrationEventContracts_ShouldMatch_TheFrozenSnapshotas a test of its own, and the enclosing facts invoke it as a delegate instead. - Where it's used - constructed six times across the facts of IntegrationEventContractTestsBaseTests, once per contract mutation.
ScalarOnlySpec
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:69· Level 6 · class
- What it is - the deliberately-safe counterpart to NavigatingSpec: its
Criteriafilters only on the entity's own scalar columns, the pattern the fitness function must leave alone. - Depends on - Specification<TEntity, TIdentifierType> (
private sealed class ScalarOnlySpec : Specification<FitnessDependent, int>,SpecificationFitnessTests.cs:69) and FitnessDependent. - Concept introduced - cross-references the navigation-safety concept from NavigatingSpec; this is the positive example. A predicate over the entity's own scalars (
d => d.PrincipalId == 1 && d.Flag,:71) translates to SQL on any engine and survives extraction, so the rule must not flag it. Having both a flagged and an un-flagged fixture is what makes the test prove the rule discriminates rather than just always throwing. - Walkthrough - one overridden
Criteriafiltering onPrincipalIdandFlag(:71). The test asserts the rule's exception message does not contain this type's name. - Where it's used - the "should not be flagged" input to SpecificationFitnessTests.
SoftDeleteEnforcementFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementFitnessTests.cs:15· Level 6 · class
- What it is - the meta-test for the hard-delete rule: five facts proving EF's erasing members are caught by name and declaring type, that every erasing member in a type is reported rather than just the first, that an ordinary collection
Removeis never mistaken for one, and that both allowlist granularities work. - Depends on - ArchitectureRules (
HardDeletesOnlyInAllowedTypes,SoftDeleteEnforcementFitnessTests.cs:24), its nested FixtureAssemblyMap, the four types inMMCA.Common.Architecture.Tests.HardDeleteFixtures(:1), andXunit.Sdk.XunitException(:3). - Concept - cross-references the hard-delete concept taught at FixtureEntity. The fact worth dwelling on is
HardDelete_ReportsEveryErasingMember(:30-:40), which asserts all three method namesPurge,PurgeManyandPurgeAsyncappear in one message. A rule that stopped at the first hit per type would still pass every other fact in the class, and a partial report is worse than none in a codebase-wide sweep: a developer fixes the one named site, the build goes green on the next run only because the next offender is now first, and the sweep takes as many rounds as there are call sites. The rule's collector is a nestedyield returnover every method and every instruction (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:104-:118), and this fact is what pins that. - Walkthrough - a
private const string FixtureNamespace(:17) and a readonly FixtureAssemblyMap (:19) are shared by every fact.HardDelete_OutsideTheAllowlist_IsFlagged(:21-:28): the baseline, asserted withWithMessage("*DbSetRemovingFixture*").HardDelete_ReportsEveryErasingMember(:30-:40): the completeness fact described above, with abecausenaming which EF member each one maps to.OrdinaryCollectionRemove_IsNotAHardDelete(:42-:51): the discrimination fact, asserting the wholeSoftDeletingFixturetype name is absent even though it contains a literalRemovecall.AllowlistedNamespace_SilencesTheRule(:53-:60) andAllowlistedType_SilencesOnlyThatType(:62-:74): the two granularities, the second carrying the control assertion thatExecuteDeletingFixtureis still reported (:72-:73).
- Why it's built this way - the class doc names the principle again (
:7-:14): a rule that reads IL cannot be verified by reading it, so the offending and the innocent call sites are compiled side by side and a map is pointed at them. See ADR-005 and ADR-015. - Where it's used - an independent class in the Common architecture suite; the shipped binding of the same rule over real code is SoftDeleteEnforcementTestsBase, activated here by SoftDeleteEnforcementTests with the framework's four reviewed eraser types.
NavigatingSpec
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:63· Level 6 · class
- What it is - the deliberately-unsafe fixture specification: its
Criterianavigates from the dependent into a related entity, the exact pattern the fitness function must flag. - Depends on - Specification<TEntity, TIdentifierType> (
private sealed class NavigatingSpec : Specification<FitnessDependent, int>,SpecificationFitnessTests.cs:63) and FitnessDependent / FitnessPrincipal (the entities it filters over). - Concept introduced - why cross-entity navigation in a specification is unsafe across data sources. Under database-per-service (ADR-006), a
d => d.Principal!.IsActivepredicate (:65) assumes the related entity lives in the same queryable model; once the principal is extracted to another physical source, that navigation cannot translate to SQL (and on Cosmos the cross-source navigation is degraded out of the model entirely,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:9-:15). The fitness functionSpecificationsDoNotNavigateToOtherEntitiestreats it as a violation and points at the fix: resolve the related keys first and filter on the foreign-key column instead. [Rubric §7 - Microservices Readiness] assesses whether the code stays extractable; this fixture is the negative example that proves the readiness guard fires. - Walkthrough - one member, an overridden
Criteriaexpression that dereferences thePrincipalnavigation (:65). Being parameterless matters: the rule only instantiates and inspects specifications that expose a parameterless constructor (ArchitectureRules.Specifications.cs:37-:41), so a fixture with constructor dependencies would be skipped and prove nothing. The test asserts the rule's exception message contains this type's name. - Where it's used - the "should be flagged" input to SpecificationFitnessTests; paired with ScalarOnlySpec.
PiiErasureContractFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiErasureContractFitnessTests.cs:19· Level 6 · class
- What it is - a non-vacuous §30 fitness test that pushes a representative
[Pii]data subject through the framework's own privacy machinery, proving that PII detection, redaction and masking, and in-place erasure compose end to end rather than each being verified in isolation. - Depends on - DataSubjectSample (the fixture), PiiRedactor, IAnonymizable, and Result. Externals: xUnit
[Fact], AwesomeAssertions. - Concept introduced - a contract-composition fitness function. Where the plain
[Pii] => IAnonymizablescan is structural (does every marked type also expose an erasure path), this test exercises the behavior the scan presumes. It is the pattern for proving a cross-cutting compliance contract actually holds when its parts run together. [Rubric §30 - Compliance/Privacy/Data Governance] assesses that erasure and log-masking genuinely protect subject data; this test is the framework's executable evidence. - Walkthrough - four
[Fact]s, each isolating one link in the contract.DataSubject_DeclaresPii_SoTheContractIsNotVacuous(:21-:24) assertsPiiRedactor.HasPiirecognizes the sample, so the later guards assert against something real.PiiRedactor_MasksEveryPiiMember_AndPassesThroughNonPii(:26-:35) redacts an instance and checksEmailandFullNamebecomePiiRedactor.RedactedTokenwhileIdandCitypass through unchanged (:31-:34).PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry(:37-:50) verifies neither the redacted dictionary values (:42-:44) norRedactToStringoutput (:46-:49) contain the original email or name, covering both the structured-log and the flat-string rendering paths. The fourth fact (:52-:72) asserts the sample isIAnonymizable(:56), thatAnonymize()succeeds and changes the PII fields (:59-:62), that a second call also succeeds and leaves the fields erased (idempotence,:64-:66, ADR-005), and finally that an anonymized subject still leaks no original clear text when redacted (:69-:71), proving erasure and redaction compose. - Why it's built this way - the plain
[Pii] => IAnonymizablescan is vacuous in the framework (no PII entity in Common's Domain); this test closes that gap by forcing the machinery through a stand-in subject, so the §30 guarantee is proven, not merely assumed. Consumers (MMCA.ADC'sUser) run the same contract against their real aggregates. - Where it's used - an independent test class in the Common architecture suite; its structural counterpart is
PiiConventionTests, whose own doc names this class as the non-vacuous half of the pair (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:10-:11).
PiiInventoryTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Privacy·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs:14· Level 6 · class
- What it is - a self-test for the shared
PiiInventoryTestsBaserule: it subclasses the base against a fixture assembly and a fixture privacy policy, then proves the two inventory rules actually fail on a bad policy instead of passing vacuously. - Depends on - PiiInventoryTestsBase (the base it derives from), FixturePiiSubject (the fixture
[Pii]type), and ArchitectureRules (PiiMembersHaveInventoryRows,PiiInventoryRowsMapToPiiMembers). Externals: xUnit[Fact], AwesomeAssertions. - Concept introduced - testing the fitness rule, not the code. A fitness rule that has only ever seen a clean policy cannot be trusted. The class overrides
Assemblies(:18) andPrivacyPolicyPath(:20) to point at the fixture, then feeds the rule deliberately broken markdown files from aPiiInventoryFixturesfolder (:67-:68) and asserts the exact violation text. It is the same discipline as IntegrationEventContractTestsBaseTests, applied to the §30 privacy inventory. - Walkthrough - four
[Fact]s.MissingRow_Fails(:33-:40) runsPiiMembersHaveInventoryRowson a policy lacking thePhonerow and expects a message containingFixturePiiSubject.Phone has no row.RowWithEmptyLawfulBasis_Fails(:42-:51) expects the emptyLawful basiscell to be reported forPhonewhileEmail, whose row is complete, is not named.OrphanRow_Fails(:53-:60) runs the reverse rulePiiInventoryRowsMapToPiiMemberson a policy listingFaxand expectsnames no live [Pii] member.OrphanRowFixture_StillCoversEveryMember(:62-:65) guards the orphan fixture itself: the forward rule must not throw on it, so only the reverse direction can fail. - Why it's built this way - the two rules are bidirectional (every
[Pii]member has a policy row, every row maps to a live member), so each direction needs its own failing fixture, and each fixture must isolate a single defect. Asserting on message fragments with a reason string keeps a failure self-explanatory. See ADR-015. - Where it's used - an independent test class in the Common architecture suite; no other file references it.
SpecificationFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13· Level 7 · class
- What it is - the test that verifies the
SpecificationsDoNotNavigateToOtherEntitiesfitness function actually discriminates: it must flag a specification that navigates into another entity, must leave a scalar-only specification alone, and must reach both shapes through theQuerySpecificationsubclass as well. - Depends on - ArchitectureRules (the rule under test), and its own nested fixtures SpecTestMap, FitnessDependent, FitnessPrincipal, NavigatingSpec, ScalarOnlySpec, and the two query variants (NavigatingQuerySpec, ScalarOnlyQuerySpec).
- Concept introduced - testing the test: verifying a fitness function is neither vacuous nor over-broad. A rule that never fires is useless; a rule that flags everything is worse. This class proves the specification-navigation guard does exactly one thing by feeding it both a positive and a negative fixture in a single run. [Rubric §14 - Testability] assesses whether the guardrails themselves are trustworthy; this is the meta-test that earns that trust, and ADR-015 is the decision that makes such guardrails build-gating in the first place. Compare ModuleConformanceTestsBaseTests, which applies the same discipline to the module-conformance base.
- Walkthrough - two
[Fact]s, both driving the same single rule execution shape.Rule_FlagsNavigatingSpecification_ButNotScalarSpecification(:15-:24) wraps the rule call in anactdelegate,ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(new SpecTestMap())(:18), captures the thrown exception throughShould().Throw<Exception>().Which(:20), and asserts its message containsNavigatingSpecand the word "navigates" while not containingScalarOnlySpec(:21-:23). Both halves matter: the first two assertions prove the rule fires, the third proves it does not over-report.Rule_AlsoAnalyzesQuerySpecifications(:26-:38) repeats the pair one level down the hierarchy, assertingNavigatingQuerySpecis reported andScalarOnlyQuerySpecis not (:32-:37). The nested types below the facts supply the model:SpecTestMap(:40), the two entities (:48,:57), and the four specifications (:63,:69,:76,:89). - Why it's built this way - the navigation rule protects future extraction (a navigating specification cannot cross a data-source boundary, ADR-006); a discriminating test keeps the rule honest as it evolves, and the second fact keeps it honest as the specification hierarchy evolves.
- Where it's used - an independent class in the Common architecture suite.
SpecTestMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40· Level 7 · class
- What it is - a minimal architecture map used only by SpecificationFitnessTests: it registers this test assembly as the single Application layer so the specification-navigation rule has a model to scan.
- Depends on - ArchitectureMapBase (
private sealed class SpecTestMap : ArchitectureMapBase,SpecificationFitnessTests.cs:40), the Layer enum, and LayerRef. - Concept introduced - cross-references the map concept from CommonArchitectureMap. Where the real map spans seven packages, this one collapses to a single self-referential Application layer (
Framework(Layer.Application, typeof(SpecificationFitnessTests).Assembly),:45) because the fixtures (the four specifications and two entities) live in the test assembly itself. It is the smallest map that lets a fitness function run against hand-crafted types; the rule scans Application and Domain layers for specification subclasses (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:30-:33), so one Application entry is enough. - Walkthrough -
RepoToken => "MMCA.Common"(:42) and a one-entryDefineLayers()(:44-:45) pointing at this assembly. - Where it's used - instantiated once per fact inside SpecificationFitnessTests (
:18,:29).
BadgeGranter, DirectSavingHandler, PointsWriter
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures· (see per-type table) · Level 8 · class
The three fixture types that actually call IUnitOfWork.SaveChangesAsync. Each is an internal sealed class with one injected IUnitOfWork, one constructor, and one method whose whole body is the save. They are the terminals of every call chain the domain-event handler save rule has to find.
- Depends on - IUnitOfWork (
DomainEventSaveFixtures.cs:2), plus the handler interface and event for the one that is itself a handler. - Concept - cross-references the handler-save concept from IBadgeGranter. What this trio pins is how the rule recognizes a save at all, which is broader than a single interface.
IsSaveCallaccepts a callee namedSaveChangesorSaveChangesAsync(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:12) whose declaring type is either under theMicrosoft.EntityFrameworkCorenamespace prefix, or whose simple name ends with one of four suffixes:DbContext,DbContextFactory,UnitOfWorkorRepository(:20-:21,:235-:255). Matching by suffix rather than by a full type name is what lets the rule see EF's own contexts, the framework'sDbContextFactorysave surface, a consumer'sSQLServerDbContext, and anyIRepositorythat forwards to one, without the rule package taking a compile dependency on any of them. These three fixtures all reach it throughIUnitOfWork, the suffix case. - Walkthrough - all three share the same shape: a
private readonly IUnitOfWork _unitOfWork, aninternalconstructor assigning it, and one method returning or awaiting_unitOfWork.SaveChangesAsync(cancellationToken). What differs is where they sit in a chain.
| Type | File:Line | Its position in a chain |
|---|---|---|
DirectSavingHandler |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:16 |
Depth zero: the handler saves in its own HandleAsync (:22-:23). Caught at the call site with no traversal needed, which is what makes it the control for the depth-bound and allowlist facts, since neither can hide it. |
PointsWriter |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:53 |
Depth two: the second hop of the transitive chain, reached only through PointsAwarder. Its WriteAsync is async and awaits with ConfigureAwait(false) (:59-:60), so the call the walk must find lives inside a compiler-generated state machine rather than in the method body. |
BadgeGranter |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:70 |
Behind an abstraction: the sole implementation of IBadgeGranter, reached from InterfaceDispatchSavingHandler only after the walk expands the interface call to its targets. Also async (:76-:77). |
- Why they're built this way - three different routes to the same terminal is the design. If all three saves were direct, the walk's traversal, its state-machine handling and its interface resolution would all be untested; if the direct one did not exist, nothing would prove that a save at depth zero is caught independently of the walk at all, which is the assertion the allowlist and depth-bound facts rest on.
- Where they're used -
DirectSavingHandleris named by three facts of DomainEventHandlerSaveFitnessTests (:30,:92,:103);PointsWriteris asserted to appear in the reported chain (:43);BadgeGranteris reached transitively and is the reasonInterfaceDispatchSavingHandleris reported at all.
ReadRepositoryQueryHandlerFixture, WriteRepositoryCommandHandlerFixture, WriteRepositoryQueryHandlerFixture
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.ReadRepositoryFixtures· (see per-type table) · Level 8 · class
Three compiled handler fixtures proving QueryHandlersUseReadRepositories, the rule that a query handler must call IUnitOfWork.GetReadRepository, never GetRepository (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:12-:16,:37). One is the compliant query handler, one is a command handler that is legitimately out of the rule's scope, and one is the violation: a query handler that reaches for the write repository from behind an await.
- Depends on - IUnitOfWork, IQueryHandler<in TQuery, TResult> and ICommandHandler<in TCommand, TResult>, and
ReadRepositoryFixtureAggregate/ReadRepositoryFixtureQuery/ReadRepositoryFixtureCommand(ReadRepositoryFixtures.cs). - Concept - cross-references the call-graph-walk concept taught at FixtureAssemblyMap. This trio pins two things about the rule at once: that it discriminates by handler kind (a query handler is scanned, a command handler with the identical
GetRepositorycall is not, because the rule only walks types implementingIQueryHandler<,>) and that it sees through anasyncbody.WriteRepositoryQueryHandlerFixtureputs its offending call afterawait Task.Yield()(:26-:29) specifically so the violation sits inside the compiler-generated state machine rather than the declared method body, which is what the rule's IL reader has to resolve to catch it at all. - Walkthrough - each is an
internal sealed classwith a primary-constructor-injectedIUnitOfWorkand a singleHandleAsync.ReadRepositoryQueryHandlerFixture(ReadRepositoryFixtures.cs:29-:33):IQueryHandler<ReadRepositoryFixtureQuery, Result<int>>callingunitOfWork.GetReadRepository<ReadRepositoryFixtureAggregate, int>()(:30) thenCountAsync(:31). The compliant shape.WriteRepositoryCommandHandlerFixture(:40-:49):ICommandHandler<ReadRepositoryFixtureCommand, Result>callingunitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>()(:59) then discarding aCountAsync(:60). Legitimate, because a command handler is the write side.WriteRepositoryQueryHandlerFixture(:17-:26):IQueryHandler<ReadRepositoryFixtureQuery, Result<int>>that awaitsTask.Yield()(:20) before callingunitOfWork.GetRepository<ReadRepositoryFixtureAggregate, int>()(:21), the violation the rule must still catch post-await.
- Why they're built this way - three fixtures rather than one prove the rule fires on the true violation, stays silent on the two shapes that must not be flagged (a compliant query handler and any command handler at all), and does so even when the call is compiler-relocated into a state machine.
- Where they're used - the compiled input to QueryHandlerReadRepositoryTests:
QueryHandlerAskingForTheWriteRepository_IsFlagged_EvenInsideAnAsyncBodyandAllowlistedHandler_IsSilencedassert onWriteRepositoryQueryHandlerFixture(QueryHandlerReadRepositoryTests.cs:23-:26,:38-:41),QueryHandlerAskingForTheReadRepository_IsNotFlaggedassertsReadRepositoryQueryHandlerFixtureis absent (:28-:30), andCommandHandlerAskingForTheWriteRepository_IsOutOfScopeassertsWriteRepositoryCommandHandlerFixtureis absent (:32-:36).
PointsAwarder
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:42· Level 9 · class
- What it is - hop one of the transitive save chain: a service that saves nothing itself and delegates to PointsWriter, which does.
- Depends on - PointsWriter (constructor-injected,
DomainEventSaveFixtures.cs:44,:46). - Concept - cross-references IBadgeGranter. Its distinctive job is to be the middle of a chain, which is what makes it the natural subject of the allowlist-pruning fact. Allowlist entries in this rule do two things at once: a matching type is neither reported nor walked into (rule doc,
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:72-:74), becauseEnqueueTargetsfilters targets by the same allowlist before queueing them (:170-:172). Allowlisting a collaborator therefore stops the walk at that node and the handler behind it disappears from the report, while a handler that saves directly is unaffected because its save is found at the call site rather than through a target expansion. That distinction is exactly what one fact pins. - Walkthrough - one field, one
internalconstructor, andinternal Task AwardAsync(CancellationToken cancellationToken) => _writer.WriteAsync(cancellationToken);(:48-:49). It is a plain task-returning pass-through, not anasyncmethod, so it generates no state machine of its own. Beinginternalrather than public is irrelevant to the rule, which reads Cecil type definitions rather than reflected public surface. - Where it's used - asserted to appear in the reported chain as the first hop (
DomainEventHandlerSaveFitnessTests.cs:42), and used as the allowlist entry inAllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves(:81).
TransitiveSavingHandler
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.DomainEventSaveFixtures·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/DomainEventSaveFixtures/DomainEventSaveFixtures.cs:31· Level 10 · class
- What it is - the real-world offender shape: a handler that delegates to a service, which delegates to a writer, which saves. Two hops, of which only the last (
PointsWriter.WriteAsync) is anasyncmethod. - Depends on - IDomainEventHandler<in TDomainEvent> closed over FixtureDomainEvent, and PointsAwarder (
DomainEventSaveFixtures.cs:31,:33). - Concept - cross-references IBadgeGranter. This is the fixture that forces the two hardest parts of the walk. First, the last hop (
PointsWriter.WriteAsync,DomainEventSaveFixtures.cs:59) isasync, so the save call the walk needs is not in the declared method body: the C# compiler moves it into a generated state-machine type. The handler andPointsAwarderhops are plain task-returning pass-throughs (:37-:38,:48-:49), so their calls sit in the declared bodies. The rule handles that by resolving each method'sAsyncStateMachineAttributeto its generated type and reading that type's methods too (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:200-:231), then collapsing those generated types back onto their declaring type for the report so the printed chain reads like source (:265-:277). The fixture doc (:26-:29) still words it as both hops beingasync, but the point stands for the writer hop: the walk must follow the compiler-generated state machines to see the save. Second, the walk is breadth-first with a visited set and a depth bound (:120-:158), which is what makes it cycle-safe and what makes the depth-bound fact possible. - Walkthrough - one field, one
internalconstructor, andpublic Task HandleAsync(...) => _awarder.AwardAsync(cancellationToken);(:37-:38), a non-asyncpass-through. The reported chain for it names three nodes and the save: the handler,PointsAwarder,PointsWriter, andSaveChangesAsync. - Where it's used - the subject of
TransitiveSave_TwoHopsAway_IsFlagged(DomainEventHandlerSaveFitnessTests.cs:34-:45), which asserts all four names appear, and the control in bothAllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves(:88-:90) andDepthBound_StopsTheWalk(:104-:106), where it must not appear.
DomainEventHandlerSaveFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/DomainEventHandlerSaveFitnessTests.cs:17· Level 11 · class
- What it is - the meta-test for the domain-event-handler save rule: seven facts pinning a direct save, a two-hop transitive save with its full chain, an interface call resolved to its implementation, a clean handler, both allowlist behaviors, and the depth bound.
- Depends on - ArchitectureRules (
DomainEventHandlersDoNotSave,DomainEventHandlerSaveFitnessTests.cs:26), its nested FixtureAssemblyMap, the eight types inMMCA.Common.Architecture.Tests.DomainEventSaveFixtures(:1), andXunit.Sdk.XunitException(:3). - Concept introduced - a call-graph walk, and the three things a test of one has to pin. The rule is the most machinery-heavy in the suite: it reads every scannable assembly into Mono.Cecil, builds a CallGraphIndex over them, finds every type implementing the domain-event-handler interface, and breadth-first searches outward from each of its methods for a save, reporting the shortest chain it finds (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93-:109,:125-:158). Three properties of such a walk need proof and none is visible by reading it. That it reaches far enough: the transitive fact. That it resolves indirection: the interface fact. That it stops: the depth-bound fact. The last is the one teams usually skip, and it matters twice over here, becausemaxCallDepthdefaults to 6 (:79) and a walk with no bound over a real repo's assemblies would not terminate usefully. The reported chain is itself part of the contract, which is why the transitive fact asserts on every node rather than on the handler alone: a report that named only the handler would leave a developer to rediscover the path by hand. [Rubric §6 - CQRS & Event-Driven] is the property; [Rubric §12 - Performance & Scalability] is why the bound exists; [Rubric §14 - Testability] is the technique. - Walkthrough - a
private const string FixtureNamespace(:19) and a readonly FixtureAssemblyMap (:21) are shared by every fact.DirectSave_InTheHandler_IsFlagged(:23-:32) andTransitiveSave_TwoHopsAway_IsFlagged(:34-:45): the two reach cases, the second asserting the handler, both hops andSaveChangesAsyncall appear, each with abecausenaming what that node proves.SaveBehindAnInterface_IsResolvedToItsImplementation(:47-:56): the indirection case.HandlerThatOnlyMutates_IsNotFlagged(:58-:67): the discrimination case.AllowlistedNamespace_SilencesTheRule(:69-:76): the whole fixture namespace exempted, assertedNotThrow, with thebecausenaming the real use, a cascade a repo accepts while migrating.AllowlistedCollaborator_PrunesTheWalkWithoutHidingDirectSaves(:78-:94): the sharpest fact in the class. Allowlisting PointsAwarder removes TransitiveSavingHandler from the report because the walk stops at the pruned node, and leavesDirectSavingHandlerreported because a direct save is detected at the call site. Both halves are asserted, which is what distinguishes a working prune from a broken rule.DepthBound_StopsTheWalk(:96-:107): calls the rule withmaxCallDepth: 1and asserts the direct save is still found while the two-hop save is not, with thebecausecalling the bound the documented limit of the walk.
- Why it's built this way - every fixture shape maps to one branch of the walk, and the allowlist and depth facts each carry a control assertion so a rule that simply stopped working could not pass them. The rule exists because a handler that saves opens a second write inside the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back (rule doc,
:23-:29). Handlers mutate state and let the owning unit of work flush it; anything needing an independent write belongs on the outbox (ADR-003). - Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainEventHandlerSaveTestsBase.
CommonArchitectureMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15· Level 12 · class
- What it is - the architecture map for the MMCA.Common framework: it names each package's layer and pins the layer to a concrete assembly, so the shared rule library knows which assembly is Shared, Domain, Application, and so on for this repo.
- Depends on - ArchitectureMapBase (
internal sealed class CommonArchitectureMap : ArchitectureMapBase,CommonArchitectureMap.cs:15), the Layer enum, LayerRef, and one anchor type per package (Result,BaseEntity<>,DomainEventDispatcher,ApplicationDbContext,ApiControllerBase,ResultGrpcExtensions,UISharedAssemblyReference,:21-:27). - Concept introduced - the map as the single point of repo-specific truth for architecture rules. The rule bodies live once in
MMCA.Common.Testing.Architectureand are parameterized by an IArchitectureMap; each repo supplies exactly one map so the same rules run identically across Common, Store, and ADC. Because Common is a module-less framework, every layer is registered as a framework layer via theFramework(...)helper rather than a module layer, and that distinction is load-bearing well beyond bookkeeping: several rules branch onmap.ModuleNames.Countto decide whether they are judging a framework or a consumer (see EventScopeFitnessTests), and several others read only the per-module accessors, which is why the self-tests for those use FixtureModuleMap instead of this one. [Rubric §3 - Clean Architecture] assesses whether layer boundaries are explicit and enforced; this map is the machine-readable statement of those boundaries. - Walkthrough -
RepoToken => "MMCA.Common"(:17) identifies the repo and is what the source-scanning rules use to locate the repo root (they look for{RepoToken}.slnx).DefineLayers()(:19-:28) returns oneFramework(Layer.X, anchorType.Assembly)entry per package, using a single anchor type to resolve each assembly (mirrors the oldPackageAssemblieshelper): Shared, Domain, Application, Infrastructure, Api, Grpc, and Ui (:21-:27). The doc comment (:8-:13) records a deliberate omission:MMCA.Common.UI.Maui(ADR-042) is absent because its four MAUI TFM assemblies cannot load in the ubuntu net10.0 test process, so its UI-plus-Shared boundary is enforced at compile time byEnforceUIMauiLayerBoundaryinSource/Build/MMCA.Common.LayerEnforcement.targetsand the windowsbuild-mauiCI job instead. - Why it's built this way - one map per repo keeps the rule bodies DRY and identical everywhere (see the "Architecture Enforcement" section in
MMCA.Common/CLAUDE.md); anchoring by type keeps the assembly reference refactor-safe. - Where it's used - supplied as
Mapby every thin*ConventionTestssubclass in this unit, used directly by EventScopeFitnessTests as the module-less contrast case (EventScopeFitnessTests.cs:39), by EventUpcasterFitnessTests as the map that owns no upcasters (EventUpcasterFitnessTests.cs:62) and by DomainThrowFitnessTests as the framework's own ADR-013 reference run (DomainThrowFitnessTests.cs:143). It is also the pattern the single-layer fitness maps (SpecTestMap, IdempotencyTestMap, CancellationTestMap, CycleTestMap, FixtureAssemblyMap) collapse.
FrameworkModuleArchitectureMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain.EntityModel·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/FrameworkModuleArchitectureMap.cs:16· Level 12 · class
- What it is - a second architecture map for the framework repo that registers MMCA.Common's own Shared, Domain and Application assemblies as one business module named
"Framework", so the module-scoped DDD rules have real subjects in a repo that otherwise declares no modules. - Depends on - ArchitectureMapBase (
internal sealed class FrameworkModuleArchitectureMap : ArchitectureMapBase,FrameworkModuleArchitectureMap.cs:16), the Layer enum, LayerRef, and four anchor types:Result(:25),BaseEntity<>(:26),DomainEventDispatcher(:27) andApplicationDbContext(:28). - Concept introduced - the same assemblies, declared twice, because the declaration decides which rules can see them. CommonArchitectureMap registers every package as a framework layer, which is the correct shape for layering and purity rules but leaves any rule that reads only IArchitectureMap's
ModuleDomain,ModuleSharedandModuleApplicationaccessors with nothing to judge. The doc comment names those rules (sealed entities, no public setters, no public aggregate constructors, immutable DTOs, commands, queries and domain events) and the aggregates they then run over: the Notifications family, PushNotification and UserNotification (:5-:15). Without this map those facts would pass vacuously in the framework repo, and the framework would ship DDD rules it does not itself obey. [Rubric §4 - DDD] is the property; [Rubric §34 - Architecture Governance & Documentation] covers the framework holding itself to the bar it exports. - Walkthrough -
public const string ModuleName = "Framework"(:19) is the module label.RepoToken => "MMCA.Common"(:21) matches the primary map.DefineLayers()(:23-:29) returns threeModule(ModuleName, Layer.X, ...)entries for Shared, Domain and Application (:25-:27) and oneFramework(Layer.Infrastructure, ...)entry (:28). Infrastructure stays a framework layer on purpose: the rules only read it to prove entities and DTOs do not live there (:13-:14). Api, Grpc and Ui are not registered at all, unlike CommonArchitectureMap. - Why it's built this way - adding module entries to CommonArchitectureMap would flip every rule that branches on
map.ModuleNames.Countinto judging the framework as a consumer; a separate map scoped to the two DDD bindings that need it keeps that switch untouched. - Where it's used - supplied as
Mapby exactly two classes in the same folder: EntityConventionTests (EntityConventionTests.cs:14) and ImmutabilityTests (ImmutabilityTests.cs:13).
FrameworkSanityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FrameworkSanityTests.cs:13· Level 12 · class
- What it is - the home for the few architecture checks that are Common-only and do not generalize into the shared rule library: the
MMCA.Common.Grpctransport boundary and the placement of theIMessageBus,IJwksProvider, andILiveChannelPublisherabstractions. - Depends on - IMessageBus, IJwksProvider, ILiveChannelPublisher, and the NetArchTest
Typesquery API routed through ArchitectureAssert. - Concept introduced - repo-specific sanity next to the shared library. Not every rule fits the parameterized base classes; some assert facts true only of the framework repo. Keeping them in one explicitly-named class documents the boundary between "shared rule applied here" and "Common-only invariant." [Rubric §7 - Microservices Readiness] (transport isolation) and [Rubric §3 - Clean Architecture] (abstraction placement) both apply: gRPC is pure transport and must not couple to Domain, Application, or Infrastructure, and the cross-cutting abstractions must sit in the layer their consumers depend on.
- Walkthrough - three private static
Assemblyaccessors anchor the Grpc, Application, and Infrastructure assemblies by an anchor type each (:15-:19). Three[Fact]s assertMMCA.Common.Grpchas no dependency on Domain, Application, or Infrastructure (:21-:34) via theAssertNoDependencyhelper (:51-:59), which runs aTypes.InAssembly(...).ShouldNot().HaveDependencyOnAny(...)NetArchTest query and routes the result throughArchitectureAssert.NoViolations(:58). Three more[Fact]s assert placement by comparing the abstraction's declaring assembly against the anchored layer assembly:IMessageBuslives in Application (:36-:39),IJwksProviderin Infrastructure because it handles crypto and PEM material (:41-:44), andILiveChannelPublisherin Application besideIPushNotificationSender(:46-:49). - Why it's built this way - the message-bus abstraction must stay in Application so application code depends on transport through it (extraction boundary, ADR-007); the JWKS provider is crypto and belongs in Infrastructure (ADR-004). These are load-bearing placements, so they get their own asserted facts.
- Where it's used - an independent class in the Common architecture suite; it has no counterpart in Store or ADC because only Common owns the Grpc package and defines these abstractions.
ModuleIsolationTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleIsolationTestsBaseTests.cs:16· Level 12 · class
- What it is - coverage of the module-isolation rule set itself, and in particular of the one rule that closes a gap the six named rules leave open: a module's Domain reaching another module's Application.
- Depends on - ArchitectureRules (seven isolation rules called by name,
ModuleIsolationTestsBaseTests.cs:30,:48,:60-:65,:83), its nested StubMap, and the framework's Infrastructure and Domain assemblies as the two real subjects (:25,:44,:81). - Concept introduced - a cross product beats a hand-written pair list, and a test can document why. The six named isolation rules cover Domain, Application, Infrastructure and Api each against their own layer in another module, plus Domain and Application against another module's Infrastructure (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Modules.cs:7-:29). That leaves ten pairs unchecked, and one of them matters: aSales.Domain -> Catalog.Applicationproject reference passed every gate while compile-coupling two modules that ADR-007 and ADR-008 promise can be extracted separately. The per-module layer rules do not catch it because they forbid only the same module's higher layers, and the compile-time layer guard does not because it only knowsMMCA.Common.*references (rule doc,:34-:41; class doc,:8-:14).ModuleInternalLayersAreIsolated(:47) closes it by iterating the full four-by-four product, withLayer.Uideliberately excluded because a module's UI composing another module's UI is a real, intended arrangement in the shipped apps (:42-:45). MMCA.Common declares no modules, so every one of these rules is vacuous against the real map, which is exactly why a stub map is what makes the pairs assertable at all. [Rubric §7 - Microservices Readiness] is the property (ADR-059); [Rubric §34 - Architecture Governance & Documentation] is the second fact's contribution; [Rubric §14 - Testability] is the technique. - Walkthrough - a static readonly StubMap named
CrossLayerViolation(:21-:25) declares module"Beta"owning one Domain assembly, and namesMMCA.Common.Applicationas another module's Application layer. The assembly it registers is the framework's Infrastructure assembly, which really does depend on that namespace, so the violation is genuine rather than simulated (comment,:18-:20).ModuleInternalLayersAreIsolated_CatchesADomainReachingAnotherModulesApplication(:27-:33): asserts the cross-product rule throws.ModuleInternalLayersAreIsolated_CatchesAUiOrSharedReachingAnotherModulesApplication(:35-:51, M194): a[Theory]overLayer.UiandLayer.Shared(:37-:39). It builds aStubMapwhose source layer is that layer, with the same forbiddenMMCA.Common.Applicationnamespace, and asserts the cross-product rule throws. A module's UI or Shared reaching another module's Application is the same breach as Domain doing it, so both are sources the product must cover (as targets they stay legal, per the test comment at:35-:36); the failure message says a module must reach another only through its Shared contracts.TheSixNamedRules_DoNotCoverThatPair(:53-:72): the unusual one. It calls all six named rules against the same violating map and asserts each one does not throw. Its inline comment states the purpose (:56-:57): it documents exactly why the cross-product rule exists, and if one of the six ever does start covering the pair, this test says so and the new rule can be re-scoped. That is a test whose failure is a prompt to think rather than a defect report.ModuleInternalLayersAreIsolated_IsSatisfiedWhenNothingCrossesAModuleBoundary(:74-:86): a second stub map naming a namespace nothing references, over the framework's Domain assembly, assertedNotThrow. The discrimination half.
- Why it's built this way - proving a gap requires asserting six negatives, which is only legible because they are listed as an array of
Actionand looped (:58-:71). Writing them as six separate facts would have buried the point. See ADR-015. - Where it's used - an independent class in the Common architecture suite. The rules it covers are bound for real by ModuleIsolationTestsBase in the two module-bearing consumer repos; MMCA.Common has no modules, so it runs none of them outside this file.
FrameworkModels
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:55· Level 13 · class
- What it is - a private static helper nested inside DeleteBehaviorConventionTests that builds a real, compiled EF Core model for each of the framework's two production engines, so the delete-behavior convention rule can inspect the SQLite model and the SQL Server model rather than reason about attributes in the abstract.
- Depends on - SqliteDbContext and SQLServerDbContext (
:63,:78), PhysicalDataSource andDataSourceKey.Default(:67,:82), AuditSaveChangesInterceptor and DomainEventSaveChangesInterceptor (:91-:95), IEntityDataSourceRegistry (:96), andMicrosoft.EntityFrameworkCore.DbContextOptionsBuilderfrom the BCL. - Concept introduced - standing up the minimum viable
ApplicationDbContextfor a model-only assertion. ReadingOnModelCreatingoutput requires a constructed context, but constructing one drags in every serviceOnConfiguringdemands.Services()(:88-:98) registers only what that base method actually resolves: anAuditSaveChangesInterceptorand aDomainEventSaveChangesInterceptor, both handed an explicitTimeProvider.System, the latter wired to aNoDomainEventDispatcherand aNoOutboxSignal, plus aNoEntityDataSourcesregistry, each a do-nothing fake bound through the real interfaces. Nothing here touches a database;Sqlite()opensDataSource=:memory:andSqlServer()points at an unreachable connection string, because building the model does not execute a connection. [Rubric §34 - Architecture Governance & Documentation] is the property: a convention test that asserts againstcontext.Modelis asserting against the same object EF Core migrations generate from, which is a stronger guarantee than pattern-matching source text. - Walkthrough - two public factory methods and one private helper.
Sqlite()(:57-:70) andSqlServer()(:72-:85) each build aDbContextOptionsBuilder<T>, construct the matching sealed context (SqliteDbContextorSQLServerDbContext) with aPhysicalDataSourcekeyed byDataSourceKey.Default(DataSource.Sqlite|SQLServer), and returncontext.Modelafter disposing the context.Services()(:88-:98) builds the sharedServiceProviderboth factories pass in, passingtimeProvider: TimeProvider.Systemexplicitly toDomainEventSaveChangesInterceptor(:91-:95) rather than relying on its default. - Why it's built this way - engine parity is the point: the same convention rule runs against both models from the same fixture setup, so a delete-behavior default that only SQLite or only SQL Server gets right would show up as a two-model mismatch rather than pass silently on whichever engine the CI runner happened to exercise.
- Where it's used - called from DeleteBehaviorConventionTests's
[Theory]cases to source the model each case asserts against; not used outside that file.
EventVersioningConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12· Level 13 · class
- What it is - the two-line binding of the integration-event convention rules (SchemaVersion,
BaseIntegrationEventinheritance, namespace residency; ADR-010) to MMCA.Common's own map. - Depends on - EventConventionTestsBase (
public sealed class EventVersioningConventionTests : EventConventionTestsBase,EventVersioningConventionTests.cs:12) and CommonArchitectureMap (:14). - Concept introduced - none new; the same shipped-binding shape FeatureFlagLifecycleTests below and every other
*TestsBasesubclass in this suite follow. The class doc names the one concrete event this binding proves against today:OutputCacheEvictionRequestedinMMCA.Common.Domain.IntegrationEvents(:8-:10); EventUpcasterFitnessTests above proves the upcaster half of the same rule family against fixtures because the framework ships no upcaster of its own. [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties; [Rubric §34 - Architecture Governance & Documentation] is the shipped-binding discipline (ADR-010). - Walkthrough - one member,
Map(:14), overridden tonew CommonArchitectureMap(); the base class's facts run againstMMCA.Common.Domain.IntegrationEvents. - Why it's built this way - the class doc states its own failure mode directly, that the rules fail the build the moment a new integration event ships that breaks the convention, rather than only surfacing when an old outbox row is replayed (
:9-:10). - Where it's used - an independent class in the Common architecture suite; the same base is subclassed by MMCA.ADC and MMCA.Store with their own maps.
HandlerResultConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12· Level 13 · class
- What it is - the two-line binding of the handler-result convention to MMCA.Common's own Application layer: every concrete command/query handler's
TResultmust beResultorResult<T>. - Depends on -
HandlerResultConventionTestsBase(public sealed class HandlerResultConventionTests : HandlerResultConventionTestsBase,HandlerResultConventionTests.cs:12) and CommonArchitectureMap (:14). The class doc namesResultFailureFactory'sInvalidOperationExceptionas the runtime-only enforcement of the same constraint (:9-:10). - Concept introduced - catching at build time what would otherwise only surface as a runtime exception. Without this rule, a handler declared with a non-
ResultTResultis invisible until the decorator pipeline'sResultFailureFactorythrows against it in production, which is what the class doc calls out directly. Running the check over the framework's own Notifications command/query handlers is the same self-application pattern DomainThrowFitnessTests uses for the Domain layer. [Rubric §6 - CQRS & Event-Driven] is the property (a uniformResult-based handler contract the decorator pipeline and error catalog both assume); [Rubric §34 - Architecture Governance & Documentation] is catching the drift in CI rather than at first request. - Walkthrough - one member,
Map(:14), overridden tonew CommonArchitectureMap(); the[Fact]s that scan every concreteICommandHandler/IQueryHandlerimplementation for a non-conformingTResultlive in the base. - Why it's built this way - the same one-class-per-repo shape every
*TestsBasesubclass in this suite follows (ADR-015); the constraint itself is ADR-013's Result pattern applied at the handler boundary. - Where it's used - an independent class in the Common architecture suite; the rule and its fixtures live entirely in the base, exercised for real against MMCA.Common's own Notifications handlers.
IdempotencyConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10· Level 13 · class
- What it is - the two-line binding of the idempotency convention gate to MMCA.Common's own API layer: every POST it ships must either replay on an
Idempotency-Keyor state in code why it must not. - Depends on -
IdempotencyConventionTestsBase(public sealed class IdempotencyConventionTests : IdempotencyConventionTestsBase,IdempotencyConventionTests.cs:10) and CommonArchitectureMap (:12). - Concept introduced - none new; the class doc frames the rule as a gate rather than a blanket requirement, "either replays ... or says in code why it must not" (
:7-:8), which matches the[Idempotent]attribute mechanics described inMMCA.Common/CLAUDE.md(Idempotency:Idempotency-Keyheader, 24h replay cache,BuildCacheKeyjoining subject/method/route/client key,IDistributedLockmutual exclusion). [Rubric §11 - Security] and [Rubric §9 - API & Contract Design] are the properties (duplicate-request safety documented per endpoint); [Rubric §34 - Architecture Governance & Documentation] is the escape-hatch-with-reasons discipline the same doc comment style enforces elsewhere in this suite. - Walkthrough - one member,
Map(:12); the base class's[Fact]s enumerate every POST action in the assemblies the map registers and check for[Idempotent]or a documented exemption. - Why it's built this way - the same shipped-binding shape as every other
*TestsBasesubclass in this suite (ADR-015); the idempotency mechanics themselves are ADR-017. - Where it's used - an independent class in the Common architecture suite; MMCA.ADC and MMCA.Store subclass the same base with their own maps.
LayerDependencyTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9· Level 13 · class
- What it is - the two-line binding of the Clean Architecture layer-flow rules to MMCA.Common's own packages:
API/Grpc -> Infrastructure -> Application -> Domain -> Shared, withUI/Grpcrestricted to Shared only andUI.Websitting above both (MMCA.Common/CLAUDE.md, Architecture). - Depends on -
LayerDependencyTestsBase(public sealed class LayerDependencyTests : LayerDependencyTestsBase,LayerDependencyTests.cs:9) and CommonArchitectureMap (:11). - Concept introduced - the runtime half of a two-gate enforcement scheme.
MMCA.Common/CLAUDE.mddocuments two enforcement gates for the same layer rule: a compile-time one,Source/Build/MMCA.Common.LayerEnforcement.targets, which fails the build on a forbiddenProjectReference, and a runtime one, this NetArchTest suite, which fails the build on a forbidden namespace dependency the compile-time gate cannot see (a reflection-based call, a string-typed reference). The two are meant to move together; the doc states new rules go in both. [Rubric §3 - Clean Architecture] is the property directly asserted; [Rubric §34 - Architecture Governance & Documentation] is the two-gate discipline. - Walkthrough - one member,
Map(:11); the base class's facts assert the forward layer flow and theUI/Grpc/UI.Webexceptions over the assemblies CommonArchitectureMap registers. - Why it's built this way - the same one-class-per-repo shape every
*TestsBasesubclass in this suite follows (ADR-015). - Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
LocalizedTextConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11· Level 13 · class
- What it is - the binding of the hard-coded-literal rule to MMCA.Common's own UI package: snackbar messages, page titles,
<PageTitle>markup and breadcrumb labels inMMCA.Common.UImust resolve throughIStringLocalizer(ADR-027), not a raw string, and a<DeleteConfirmation>must receive a localizedEntityTypenoun. It also carries its own fixture-driven facts that pin how ArchitectureRules judges thatDeleteConfirmationcase. - Depends on -
LocalizedTextConventionTestsBase(public sealed class LocalizedTextConventionTests : LocalizedTextConventionTestsBase,LocalizedTextConventionTests.cs:11), CommonArchitectureMap (:15), andArchitectureRules.UserVisibleTextIsLocalized(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:62), which the fixture helper calls directly (LocalizedTextConventionTests.cs:136). - Concept introduced - a floor count as a scan-root safety valve, plus rule-behavior facts against a throwaway fixture. The class overrides
MinimumScannedFiles => 20(:18); the inline comment gives the reason:MMCA.Common.UIalone carries about 30 razor files, so a floor of 20 catches a wrong scan root, where a rule that scans zero files would otherwise report a false pass (no violations because nothing was checked). The rule enforces that floor withrazorFiles.Count >= minimumScannedFiles(ArchitectureRules.LocalizedText.cs:76). Separately, the private helperScanOneRazorFile(LocalizedTextConventionTests.cs:128) writes oneProbe.razorfile (FixtureFileName,:13) into a fresh temp directory, runs the rule over it with no allowed suffixes, and deletes the directory in afinally, so the facts test the rule's line-level judgment rather than the repo's current markup. [Rubric §27 - i18n] is the property; [Rubric §34 - Architecture Governance & Documentation] is the false-pass guard. - Walkthrough -
Map(:15) andMinimumScannedFiles(:18) bind the inherited scan to the Common source tree. The nineDeleteConfirmation_*facts and theories pin theEntityTypecheck:- a literal
EntityType="Product"fails, and the message names the fixture file and its offending line (it must contain{FixtureFileName}:1,:31) and the attributeEntityType(:26); an empty literalEntityType=""also fails (:53); - a razor expression (including the nested-quote shape
@L["Entity.Product"].Value), a variable, or an omitted attribute passes (:44); - a tag split across lines is judged as one element and the violation is reported on line 3, where the attribute sits, not the line that opens the tag (
:65); the same shape with an expression passes (:80); - the check is scoped to the
<DeleteConfirmation>element, so a literalEntityTypeon<EntityBadge>, alone or after a closedDeleteConfirmationtag, passes (:101); - a namespace-qualified tag (
MMCA.Common.UI.Components.Forms.DeleteConfirmation) still fails (:110); - a line carrying the
i18n: allowmarker passes (:120). The rule side matches: the opening-tag regex accepts an optional dotted prefix (ArchitectureRules.LocalizedText.cs:35),ScanFileskips anyi18n: allowline (:98), and it tracks the tag from its opening line until the start tag ends, yielding{relative}:{i + 1} passes a literal EntityType to DeleteConfirmation(:136-151).
- a literal
- Why it's built this way - the same one-class-per-repo shape every
*TestsBasesubclass follows, plus the floor count because a scan rule that silently scans nothing is a worse failure mode than one that reports a real violation. TheDeleteConfirmationfacts live here because the shared dialog rendersEntityTypein its title and prompt (the rule's own comment cites "Eliminar Product" for a Spanish user,ArchitectureRules.LocalizedText.cs:133-135), and a multi-line, scoped tag check is easy to regress into either a false pass or a false positive on an unrelated component. - Where it's used - an independent class in the Common architecture suite; ADC carries its own
LocalizedTextConventionTestssubclass of the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:15). The shared chrome MMCA.Common.UI ships is what every consumer's UI is built on top of, so this rule is what keeps that chrome localizable.
MicroserviceExtractionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11· Level 13 · class
- What it is - the two-line binding of the transport-boundary rule to MMCA.Common's own packages: Domain, Application and Shared must stay free of MassTransit, gRPC and Protobuf types, so application and domain code talks only to abstractions while transport choices live at the edges (
MMCA.Common/CLAUDE.md, Microservices Extraction Boundaries). - Depends on -
MicroserviceExtractionTestsBase(public sealed class MicroserviceExtractionTests : MicroserviceExtractionTestsBase,MicroserviceExtractionTests.cs:11) and CommonArchitectureMap (:13); the class doc points to its Common-specific sibling,FrameworkSanityTests, for the Grpc package's own boundary and abstraction-placement checks that the shared base does not cover. - Concept introduced - none new;
MMCA.Common/CLAUDE.mdnames the concrete instance this rule enforces:IMessageBuslives in Application, whileInProcessMessageBusandBrokerMessageBus(MassTransit) live in Infrastructure, selected byMessageBusSettings. This class is what turns that prose boundary into a CI failure the moment Application, Domain or Shared references MassTransit directly. [Rubric §7 - Microservices Readiness] is the property being defended: the monolith-to-service Strangler Fig move (ADR-008) depends on transport being swappable at the edge without touching business logic. - Walkthrough - one member,
Map(:13); the base class's facts scan the Domain, Application and Shared assemblies the map registers for a forbidden reference toMassTransit,Grpc.Core/Grpc.Net.Client, or generated Protobuf types. - Why it's built this way - the same one-class-per-repo shape every
*TestsBasesubclass follows; keeping the framework-specific extras in a sibling class (FrameworkSanityTests) rather than widening this one keeps the shared rule identical to what Store and ADC subclass. - Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
NamespaceCycleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9· Level 13 · class
- What it is - the MMCA.Common binding of the namespace-acyclicity rule: a thin subclass that supplies the repo's map and then spends twenty-nine lines arguing, edge by edge, for the one namespace cycle the framework accepts.
- Depends on - NamespaceCycleTestsBase (
public sealed class NamespaceCycleTests : NamespaceCycleTestsBase,NamespaceCycleTests.cs:9) and CommonArchitectureMap (:11); the rule body and its fact counts live in the base, and the rule's behavior is proven against fixtures by NamespaceCycleFitnessTests. - Concept introduced - an allowlist that must name a whole strongly connected component, not an edge. A cycle-breaking exemption is the easiest place in an architecture suite to hide a design failure, so this override is written to make hiding expensive.
AllowedCycleNamespaceslists three namespaces, not three edges, because the allowance covers the entire SCC: a fourth namespace joining the tangle changes the component and still fails the test (:37-:38). And the justification sits in the compiled file next to the list, so a diff that adds a fourth entry is a diff that has to argue against three paragraphs. [Rubric §15 - Best Practices & Code Quality] is the property being defended; [Rubric §34 - Architecture Governance & Documentation] is the escape-hatch-with-reasons discipline (ADR-015). - Walkthrough - two members.
Map(:11) is the usual override.AllowedCycleNamespaces(:42-:47) returnsMMCA.Common.Infrastructure,MMCA.Common.Infrastructure.PersistenceandMMCA.Common.Infrastructure.Messaging(:44-:46), the acceptedroot -> Messaging -> Persistence -> roottangle inside a single assembly and a single package (:14-:16). Its doc comment (:13-:41) takes the three edges one at a time:root -> Messaging(:19-:20):DependencyInjectionlives in the root namespace and binds the buses and their settings, which is what a composition root is for.Messaging -> Persistence(:23-:27): the buses are the outbox transport. InProcessEventBus and BrokerEventBus enqueueOutboxMessagerows and wake the OutboxProcessor through IOutboxSignal, the consumers resolve the physical source through IDataSourceResolver, and the outbox reports throughBrokerMetricsand readsMessageBusSettings. Splitting the two would put half of one delivery guarantee on each side of a package boundary.Persistence -> root(:30-:34): theEntityTypeConfiguration*shims carry[UseDataSource]and[UseDatabase], marker attributes that stay in the root namespace because consumers annotate their own configurations with them. Pushing them down intoPersistencewould deepen the public annotation surface for every consumer in order to fix an internal graph edge.
- Why it's built this way - a comment closes with the history of the exemption itself (
:38-:40): before the 2026-09 feature-by-folder reorganization the third node wasMMCA.Common.Infrastructure.Settings, that folder dissolved into the features it configured, and TenancySettingsValidator, which carried the oldSettings -> Persistenceedge, now lives underPersistence/Tenancy. The list moved because the code moved, which is the argument for keeping the allowlist in a compiled file rather than a wiki page nobody re-reads. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob (fast, no database). Store and ADC subclass the same base with their own maps and their own allowlists.
AggregateConventionTests, CancellationTokenConventionTests, DomainPurityTests, EventVersioningConventionTests, HandlerResultConventionTests, IdempotencyConventionTests, LayerDependencyTests, LocalizedTextConventionTests, MicroserviceExtractionTests, NamespaceCycleTests, PiiConventionTests, RawQueryableConventionTests, SliceCohesionTests, StateManagementConventionTests, UIArchitectureConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain· (see per-type table) · Level 13 · class
These fifteen sealed classes share one shape: each is a thin subclass of a shared *TestsBase rule from the MMCA.Common.Testing.Architecture package, supplying the repo's CommonArchitectureMap (and, for a few, one extra override) so the same rule body runs identically across MMCA.Common, MMCA.Store, and MMCA.ADC. This is the [Rubric §34 - Architecture Governance & Documentation] and [Rubric §14 - Testability] story: architecture conventions are executable and enforced in CI rather than left to review, and the rule logic lives in exactly one place (ADR-015). See the thin-subclass pattern introduced by DependencyVersionTests. The canonical body of each rule is the corresponding *TestsBase; these subclasses only wire in the map and any repo-specific floor or allowlist. Each fails the build-and-test CI job on violation, and a couple are deliberately vacuous today (they assert nothing until the framework grows a type that could break the convention, at which point they fire).
Where a subclass carries an override beyond Map, that override is itself a documented architectural decision, not configuration: the exemption lists below (NamespaceCycleTests, CancellationTokenConventionTests, StateManagementConventionTests, RawQueryableConventionTests) all carry their justification in code, which is the point of putting the escape hatch in a compiled file rather than a wiki.
| Type | File:Line | Base rule | What it enforces / what differs |
|---|---|---|---|
AggregateConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9 |
AggregateConventionTestsBase | DDD aggregate-root factory rules for the framework's own aggregates: Domain exposes aggregate roots, each has a Result<T>-returning static Create factory and no public constructor. The minimal variant for repos with no business modules. Supplies only Map (:11). [Rubric §4 - DDD.] |
CancellationTokenConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10 |
CancellationTokenConventionTestsBase | The trailing-token convention taught above, applied to the real Application and Infrastructure packages. Supplies Map (:12) and overrides CancellationTokenExemptMethods with the two SignalR hub methods NotificationHub.JoinChannelAsync and NotificationHub.LeaveChannelAsync (:23-:27). The justification (:14-:22) is worth reading: a hub method signature is the client-visible RPC contract, bound by name and argument list by SignalR's dispatcher, so adding a parameter would break every shipped consumer's client for a token the hub already has (both methods pass Context.ConnectionAborted straight into the group calls). [Rubric §12 - Performance & Scalability.] |
DomainPurityTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9 |
DomainPurityTestsBase | Domain and Shared stay framework-free, and Application stays host-agnostic (no EF Core, no ASP.NET Core). Supplies only Map (:11); the base's extra-forbidden-dependency hook (used by Store for "Stripe", ADC for "RabbitMQ") stays empty here. [Rubric §3 - Clean Architecture.] |
EventVersioningConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12 |
EventConventionTestsBase | Every integration event declares a SchemaVersion, inherits BaseIntegrationEvent, and lives in an *.IntegrationEvents namespace (ADR-010). Supplies only Map (:14). No longer vacuous: the framework ships one concrete integration event today, OutputCacheEvictionRequested in MMCA.Common.Domain.IntegrationEvents (class doc, :7-:10). [Rubric §6 - CQRS & Event-Driven.] |
HandlerResultConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12 |
HandlerResultConventionTestsBase | Every concrete command/query handler's TResult must be Result or Result<T>, the constraint the decorator pipeline otherwise only enforces at runtime when ResultFailureFactory throws on a short-circuit (:9-:10); scans the framework's Notifications handlers. Supplies only Map (:14). [Rubric §6 - CQRS & Event-Driven.] |
IdempotencyConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10 |
IdempotencyConventionTestsBase | The POST idempotency-intent gate proven by IdempotencyFitnessTests, applied to the framework's own API layer: every POST it ships either replays on an Idempotency-Key or says in code why it must not (:6-:8). Supplies only Map (:12). [Rubric §9 - API & Contract Design, Rubric §29 - Resilience.] |
LayerDependencyTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9 |
LayerDependencyTestsBase | Clean Architecture layer-flow for the framework packages: Domain, Application, Infrastructure, Shared, and Ui each reference only what they may, plus the map-completeness facts LayerMapDeclaresLayers and ModulesDeclareLayers whose override is proven by LayerDependencyOverrideTests. Supplies only Map (:11), so the base's default required-layer set applies and the per-module half is vacuous here. [Rubric §3 - Clean Architecture.] |
LocalizedTextConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11 |
LocalizedTextConventionTestsBase | Shared MMCA.Common.UI ships no hard-coded user-visible literals: snackbar messages, page titles, <PageTitle> markup, and breadcrumb labels must resolve through IStringLocalizer (ADR-027). Supplies Map (:13) and overrides MinimumScannedFiles => 20 (:16), a floor that catches a wrong scan root (the comment notes MMCA.Common.UI alone carries about 30 razor files, :15). [Rubric §27 - i18n.] |
MicroserviceExtractionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11 |
MicroserviceExtractionTestsBase | Domain, Application, and Shared stay free of MassTransit, Grpc, and Protobuf, so a module behaves identically in-process or extracted (ADR-007, ADR-008). Supplies only Map (:12); Common-only transport sanity lives in FrameworkSanityTests instead (:7-:8). [Rubric §7 - Microservices Readiness.] |
NamespaceCycleTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9 |
NamespaceCycleTestsBase | The acyclicity rule proven by NamespaceCycleFitnessTests, applied to the real framework packages. Supplies Map (:11) and overrides AllowedCycleNamespaces with the one accepted tangle inside MMCA.Common.Infrastructure: root, .Persistence, .Settings (:39-:44). The 26-line justification (:13-:38) argues each of the three edges individually (composition root binds settings; TenancySettingsValidator takes an optional IDataSourceResolver so a bad tenant override fails the boot instead of resolving cross-tenant at runtime; the [UseDataSource] and [UseDatabase] marker attributes stay in the root namespace because consumers annotate with them) and notes that none of the three is separately extractable anyway, being one assembly and one package. Because the allowance must cover the whole strongly connected component, a fourth namespace joining the tangle still fails. [Rubric §15 - Best Practices & Code Quality.] |
PiiConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:25 |
PiiConventionTestsBase | Every domain entity declaring a [Pii] property implements IAnonymizable (ADR-005). Supplies Map (:98) and one AnonymizableSamples entry for RefreshSession (:100-:112), and adds a name-based marking rule (:114-:134), so it is no longer structurally vacuous in the framework (class doc, :6-:24); the machinery is proven non-vacuously by PiiErasureContractFitnessTests. [Rubric §30 - Compliance/Privacy.] |
RawQueryableConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13 |
RawQueryableConventionTestsBase | Bans the raw IQueryable repository surfaces in Application code via a textual scan; a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary. Because Common declares no modules, it overrides ApplicationSourceDirectories() (:18-:22) to scan the framework's own Source/Core/MMCA.Common.Application project (resolving the repo root through ArchitectureMapBase.FindRepoRoot("MMCA.Common.slnx"), :20), and overrides AllowedFiles (:25-:37) to whitelist the deliberate composition root EntityQueryService.cs and the five Notifications handlers whose cross-entity joins are the documented exception. Supplies Map at :15. [Rubric §8 - Data Architecture.] |
SliceCohesionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10 |
SliceCohesionTestsBase | Handlers and validators each sit in the same namespace as the command or query they serve, so a Notifications use-case slice stays one cohesive unit. Supplies only Map (:12). [Rubric §5 - Vertical Slice.] |
StateManagementConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11 |
StateManagementConventionTestsBase | The shared MMCA.Common.UI assembly carries no mutable static state (a static is shared across every Blazor Server circuit) and its stateful services stay scoped rather than singleton. Supplies Map (:13), carries facts proving the base's singleton scanner (:24-:100), and overrides AllowedStaticMembers to whitelist MMCA.Common.UI.Pages.Common.ErrorMessages._localizer (:21-:22), a write-once wiring point configured idempotently by the root layout, not per-user state (:15-:20). [Rubric §19 - State Management.] |
UIArchitectureConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11 |
UIArchitectureConventionTestsBase | Holds the container/presentational split with mechanical caps: *.razor.cs code-behind files and inline @code blocks each stay within the base's convention limit. Supplies only Map (:13), so the base's defaults apply. [Rubric §18 - UI Architecture.] |
- Why they're built this way - see the two-layer "Architecture Enforcement" model in
MMCA.Common/CLAUDE.md: rules are enforced at compile time (Source/Build/MMCA.Common.LayerEnforcement.targets) and at runtime here, with the runtime bodies factored into one shared package so Common, Store, and ADC stay identical. Each subclass exists only so xUnit discovers the rule in this repo's assembly with this repo's map. - Where they're used - all fifteen run in the
MMCA.Common.Architecture.Testsproject during CI'sbuild-and-testjob (fast, no database). - Caveats / not-in-source - the per-rule fact counts live in each
*TestsBase, not in these subclasses; the base sections elsewhere in this chapter are the authority on exactly what each rule asserts.
AggregateConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9· Level 13 · class
- What it is - the two-line binding of the DDD aggregate-root factory rules to MMCA.Common's own map (
AggregateConventionTests.cs:5-:9). - Depends on - AggregateConventionTestsBase (
:9) and CommonArchitectureMap (:11). - Concept - none new; the same shipped-binding shape as AiDependencyIsolationTests and FeatureFlagLifecycleTests: the rule body and its facts live once in
AggregateConventionTestsBase, and each repo supplies only its ownIArchitectureMap. - Walkthrough -
public sealed class AggregateConventionTests : AggregateConventionTestsBase(:9) overridesMapwithnew CommonArchitectureMap()(:11); it declares no members of its own. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass in this suite uses (ADR-015). - Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
AiDependencyIsolationTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/AiDependencyIsolationTests.cs:14· Level 13 · class
- What it is - the two-line binding of the model-boundary fitness rules to MMCA.Common's own map: a language-model dependency stays confined to Infrastructure and to the governed
MMCA.Common.AIpackage, checked in both directions. - Depends on - AiDependencyIsolationTestsBase (
AiDependencyIsolationTests.cs:14) and CommonArchitectureMap (:16). - Concept introduced - none new; this is the shipped binding of the base class's two rules for the framework's own assemblies, the same shape as FeatureFlagLifecycleTests two sections below.
AiDependencyIsolationTestsBasedocuments the concept: an AI SDK is one of the two ways the model boundary leaks (naming the vendor SDK directly, or takingMMCA.Common.AIas a convenience and passing aPromptContractaround as a domain type), and either turns "the app calls a model at one place" into a dependency spread through the app that cannot be versioned, evaluated, capped or swapped. [Rubric §16 - AI-Native Application Architecture] is the property this class defends for MMCA.Common itself. - Walkthrough - overrides
Mapwithnew CommonArchitectureMap()(:16); the two[Fact]s live entirely in the base class and runArchitectureRules.LanguageModelSdksStayAtTheModelBoundaryandArchitectureRules.GovernedAiPackageStaysBehindInfrastructureagainst it. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass in this suite uses (ADR-015): the rule body lives once in the shared package, and each repo supplies only its ownIArchitectureMap. - Where it's used - an independent class in the Common architecture suite; MMCA.ADC subclasses the same base with its own map (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs).
CancellationTokenConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10· Level 13 · class
- What it is - the trailing-
CancellationTokenconvention binding for MMCA.Common's own Application and Infrastructure packages, plus an override list for the one signature shape the rule cannot see through. - Depends on - CancellationTokenConventionTestsBase (
CancellationTokenConventionTests.cs:10) and CommonArchitectureMap (:12). - Concept - a SignalR hub method is not an ordinary public API: it IS the client-visible RPC contract, bound by name and argument list by SignalR's dispatcher, and every shipped consumer's JavaScript/Blazor client already invokes it with a fixed argument list. Adding a trailing token parameter would change that wire contract for a token the hub already has some other way.
CancellationTokenExemptMethodsis the class's one override (:23-:27), namingNotificationHub.JoinChannelAsyncandNotificationHub.LeaveChannelAsync: both already passContext.ConnectionAbortedinto their group calls, so the work is cancellable without a parameter the rule can find. - Walkthrough -
public sealed class CancellationTokenConventionTests : CancellationTokenConventionTestsBase(:10) overridesMapwithnew CommonArchitectureMap()(:12) and overridesCancellationTokenExemptMethodsto return the two hub method names (:23-:27). - Why it's built this way - the exemption list documents, at the point the rule would otherwise fail, exactly why a hub method is the one legitimate exception to the trailing-token convention, rather than leaving a future reader to rediscover it.
- Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps and (where needed) their own exemption lists.
DomainPurityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9· Level 13 · class
- What it is - the framework-independence rule binding for MMCA.Common's own map: Domain and Shared stay free of framework dependencies, and Application stays host-agnostic.
- Depends on - DomainPurityTestsBase (
DomainPurityTests.cs:9) and CommonArchitectureMap (:11). - Concept - none new; the same shipped-binding shape as AggregateConventionTests and AiDependencyIsolationTests: the rule body and its facts live once in
DomainPurityTestsBase, and each repo supplies only its ownIArchitectureMap. This is the class that pins Clean Architecture's inward-only dependency direction for the framework itself, since MMCA.Common has no modules whose Domain/Application layers could be checked any other way. - Walkthrough -
public sealed class DomainPurityTests : DomainPurityTestsBase(:9) overridesMapwithnew CommonArchitectureMap()(:11); it declares no other members. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass in this suite uses (ADR-015). - Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
PiiConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs:25· Level 13 · class
- What it is - the framework repo's binding of the PII rules (ADR-005, rubric section 30): a sealed subclass that inherits the
[Pii]-implies-IAnonymizablerule, adds a second, framework-only rule of its own (every string property whose name says it holds a person's email, name, phone number or address carries[Pii]), and supplies one anonymization sample so the inherited reverse rule runs against a real framework entity (class doc,PiiConventionTests.cs:6-:24). - Depends on - PiiConventionTestsBase (
public sealed class PiiConventionTests : PiiConventionTestsBase,:25), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (:98), the Layer enum it queries the map with (:142-:143),RefreshSession(theMMCA.Common.Domain.Authentity used as the sample,:103), and, by name only, PiiAttribute, IAnonymizable and PiiRedactor. - Concept introduced - a marking rule that gives a vacuous erasure rule something to check. The inherited fact,
EntitiesWithPiiProperties_ShouldImplement_IAnonymizable(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:34), only fires on a property that already carries[Pii]; a property nobody marked is invisible to it, and PiiRedactor logs that value in clear text (class doc,:14-:15). This subclass closes that gap by deciding from the property name which members hold personal data and requiring the marker on each. The name rule is deliberately narrow: a contact detail is any name containingEmail,PhoneorAddress(ContactFragments,:88), and a person's name is a property ending in one of nine names such asFirstName,DisplayNameorUserName(PersonNames,:92-:96). A bareNameis excluded on purpose, because in the framework it names a role, permission, event or file, never a person (:90-:91). [Rubric §30 - Compliance/Privacy/Data Governance] is the property (ADR-005). - Walkthrough -
Map(:98) is the usual override.AnonymizableSamples(:100-:112) overrides the base's empty default (PiiConventionTestsBase.cs:31) with one entry keyed bytypeof(RefreshSession): a static factory that callsRefreshSession.Create(...)with fixed fixture values (user id 1, a refresh token, UTC created and expiry dates in October 2026, a documentation-range IP address and a user agent,:103-:110) and returns.Value!(:111). The framework has no module Domain assembly, so the base's coverage fact scans nothing here; this sample is what keeps the reverse rulePropertiesOverwrittenByAnonymize_MustCarryPii(PiiConventionTestsBase.cs:43) exercised against a real entity in this repo (class doc,:16-:19).PersonalDataMembers(:140-:153) builds the scanned population: every type of the map's Domain layer (:142), plus the Application types whose namespace is, or sits under, one ofMMCA.Common.Application.Auth,.Usersor.Notifications(PersonalDataApplicationNamespaces,:80-:85; filter:143-:147); from those it keeps non-compiler-generated classes (:150), takes their public instance properties declared on the type itself (:151), and keeps thestring-typed ones whose name passesIsPersonalDataName(:152,:155-:157). Two facts then run over it.PersonalDataMembers_AreDiscovered_GateIsNotVacuous(:114-:118) asserts the population is non-empty, so a drifted namespace list or name rule fails rather than leaving a gate that checks nothing.PersonalDataMembers_ShouldCarry_PiiAttribute(:120-:134) collects every member with no attribute namedPiiAttribute(:27, matched by type name withinherit: true,:124-:125) and fails with the sortedDeclaringType.Propertylist in the message (:126-:133). - Why it's built this way - the Shared assembly is out of scope by construction, not by omission: it does not reference Domain, where
[Pii]lives, so a Shared DTO cannot carry the marker (class doc,:21-:22). Matching the attribute by type name rather than bytypeofkeeps the check independent of which assembly declared the attribute. The sample is built with a literal factory call rather than reflection so the test stays a statement of whatAnonymize()must overwrite. The redaction and erasure machinery the rules feed is proven end to end separately by PiiErasureContractFitnessTests (class doc,:22-:23). - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Testsin CI'sbuild-and-testjob; nothing constructs it. MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs), without the framework-only marking rule.
SliceCohesionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10· Level 13 · class
- What it is - the framework repo's binding of the vertical-slice cohesion fitness rules (rubric section 5): a three-line sealed subclass that supplies the map and inherits both
[Fact]s, so the framework's own Notifications use-case slices are held to the co-location rule. - Depends on - SliceCohesionTestsBase (
public sealed class SliceCohesionTests : SliceCohesionTestsBase,SliceCohesionTests.cs:10), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (SliceCohesionTests.cs:12). - Concept - the same base-plus-thin-subclass shape as StronglyTypedIdConventionTests and StateManagementConventionTests: the rule lives once in
MMCA.Common.Testing.Architectureand is re-run per repo by a subclass whose only job is to name the repo. A use-case slice keeps its command/query, its handler, and its validator together in one namespace, so a feature stays a cohesive unit rather than spreading across horizontalHandlers//Validators/folders (SliceCohesionTestsBase.cs:4-:8). - Walkthrough - one member:
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();(SliceCohesionTests.cs:12). The two inherited facts,Handlers_ShouldBeCoLocatedWith_TheirContractsandValidators_ShouldBeCoLocatedWith_TheirContracts, callArchitectureRules.HandlersAreCoLocatedWithTheirContracts(Map)andArchitectureRules.ValidatorsAreCoLocatedWithTheirContracts(Map)respectively (SliceCohesionTestsBase.cs:14-:20). - Why it's built this way - MMCA.Common scopes the rule to its Notifications slices, the one place the framework repo itself has command/query/handler/validator trios; ADC and Store subclass the same base scoped to their module Application layers instead (
SliceCohesionTestsBase.cs:7-:8). - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; nothing constructs it.
UIArchitectureConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11· Level 13 · class
- What it is - the framework repo's binding of the shared UI-architecture fitness rules: a three-line sealed subclass that supplies the map and inherits both
[Fact]s, so MMCA.Common's own shared pages and primitives are held to the code-behind and inline-@codecaps. - Depends on - UIArchitectureConventionTestsBase (
public sealed class UIArchitectureConventionTests : UIArchitectureConventionTestsBase,UIArchitectureConventionTests.cs:11), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (UIArchitectureConventionTests.cs:13). - Concept - cross-references the base-plus-thin-subclass shape of the shared fitness bases (see CancellationTokenConventionTestsBase and AggregateConventionTestsBase for the same arrangement): the rule is authored once in
MMCA.Common.Testing.Architectureand re-run per repo by a subclass whose only job is to name the repo.[Rubric §18]is the category the rule's own doc comment cites (UIArchitectureConventionTests.cs:6): it asks whether the container/presentational split is actually held, and this class is the mechanical answer for the framework repo. The caps are convention rather than measurement, so the base marks themvirtualand the doc comment says they should move only with a recorded decision (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:11-:12); this subclass overrides none of them. - Walkthrough - one member:
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();(UIArchitectureConventionTests.cs:13). Everything else is inherited. The map is the only thing the base needs from a repo: it takesMap.RepoToken, finds the repo root by walking up to{RepoToken}.slnx, and scans that repo'sSource/tree (UIArchitectureConventionTestsBase.cs:89,:91-:92), skippingobj/andbin/plus anyExcludedPathFragments(:97-:99, empty here since the subclass does not override:41). The two inherited facts then run:CodeBehinds_StayWithinTheLineCapglobs*.razor.csand fails any file overMaxCodeBehindLines= 400 (:44,:46,:22), first asserting at leastMinimumCodeBehindFiles= 1 file was discovered so a glob that matched nothing cannot pass vacuously (:35);RazorFiles_KeepInlineCodeBlocksSmallglobs*.razor, measures each file's inline block from the first line starting with@codeto end of file, and fails anything overMaxInlineCodeLines= 120 (:63,:65,:29). - Why it's built this way - the caps catch the failure mode directly (a ballooning code-behind is the visible symptom of page logic that belongs in an injected UI service or an extracted sub-component,
UIArchitectureConventionTestsBase.cs:5-:10), and putting them in CI makes the split enforced rather than review-enforced. Measuring the@codeblock as "the tail of the file" is a deliberate convention shortcut (:24-:27) rather than a Razor parse. - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; nothing constructs it. Its two sibling bindings are the identical subclasses in the consumer repos,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10(map: AdcArchitectureMap) andMMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9(map:StoreArchitectureMap), neither of which overrides a cap either.
StateManagementConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11· Level 13 · class
- What it is - the framework repo's binding of the state-management convention fitness functions (rubric section 19): a sealed subclass that supplies the map and one recorded exemption, so
MMCA.Common.UIitself is held to the per-circuit state model Blazor Server requires, and that also carries the facts proving the base's singleton source scanner. - Depends on - StateManagementConventionTestsBase (
public sealed class StateManagementConventionTests : StateManagementConventionTestsBase,StateManagementConventionTests.cs:11), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (StateManagementConventionTests.cs:13). - Concept - the same base-plus-thin-subclass shape as SliceCohesionTests, StronglyTypedIdConventionTests and UIArchitectureConventionTests: the rule is authored once in
MMCA.Common.Testing.Architectureand re-run per repo by a subclass whose only job is to name the repo (and, here, to record one exemption). Blazor Server shares one process across every circuit, so a mutable static member silently leaks one user's state into another's; the base holds two rules against that (StateManagementConventionTestsBase.cs:7-:15): a reflection scan of the repo'sLayer.Uiassemblies fails on any mutable static field or settable static property, and a source scan fails the build if a production UI project registers a stateful*StateService/*StateContaineras a singleton rather than scoped. - Walkthrough -
Map(:13) is the usual override.AllowedStaticMembers(:21-:22) overrides the base's empty default with one entry,MMCA.Common.UI.Pages.Common.ErrorMessages._localizer, with the reason recorded next to it in the doc comment (:15-:20): the root layout configures the sharedIStringLocalizerexactly once, idempotently, and the localizer itself resolves against the ambient UI culture per call, so no user's state leaks to another; the static API is kept because every consumer call site depends on it (ADR-027). The rest are facts of this subclass's own, which prove the base's source scanner rather than the repo; each calls the base'sprotected static FindSingletonStateRegistrations(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:92).SingletonScan_JudgesPathsBelowTheSourceRoot_AndCatchesAWrappedRegistration(:24-:47) builds a throwaway tree under the temp directory whose path deliberately contains aTestingsegment above theSourceroot (:29), writes the same line-wrappedservices.AddSingleton<\n ProbeStateService>();registration into anMMCA.Probe.UIproject and anMMCA.Probe.Domainproject (:30-:36), and asserts exactly one offender (the UI project'sRegistrations.cs, line 1) from exactly one scanned file (:38-:41), deleting the tree in afinally(:43-:46).SingletonScan_DoesNotReachAcrossAFluentChain(:51-:57) is a theory over three chained shapes, anAddSingletonof a non-state service followed by.AddScoped<IWizardStateService, ...>()on one line, across a line break, and intypeof(...)form, and expects no offender, because only the scoped call names the state service.SingletonScan_StillCatchesEverySingletonStateRegistrationShape(:60-:71) is the converse, nine inline registrations (generic with one or two type arguments, nested generic,TryAddSingleton,typeof, instance, factory lambda, and a singleton chained after a scoped call) that must each yield exactly one finding, line 1 of theRegistrations.csfixture file.SingletonScan_SkipsOnlyAWholeTestingSegment(:75-:82) pins the folder rule:MMCA.Probe.UI/TestingHelpersandMMCA.Probe.UI/Pages/LoadTestingare scanned (one offender each), whileMMCA.Probe.Testing.UIandMMCA.Probe.UI/Testingare skipped (zero). All three share the private helperScanOneUiFile(:84-:100), which writes oneRegistrations.csunder a tempSourcetree and returns the scanner's offenders. The two inherited facts,UiAssemblies_CarryNoMutableStaticStateandUiProjects_RegisterStatefulServicesScoped, run againstMMCA.Common.UIunchanged (StateManagementConventionTestsBase.cs:29,:67). - Why it's built this way - exempting the member individually rather than excluding the whole
ErrorMessagestype keeps the rule honest about exactly what is write-once versus per-request: a future mutable field added to that same class would still fail. The base's first fact asserts its UI-assembly set is non-empty before scanning (StateManagementConventionTestsBase.cs:33-:34), and its second asserts at least one UI source file was scanned (:74-:75), so a map that forgot to registerLayer.Uior a scan root that matched nothing fails loudly instead of passing vacuously. The extra facts pin scanner properties a real-repo run cannot: paths are judged relative to the source root, segment by segment, so a checkout cloned under a directory namedTestingis still scanned (StateManagementConventionTestsBase.cs:83-:86,:100-:103, and the comment at:27-:28here); the match is bounded byAddSingleton's own type-argument and argument lists, so a registration wrapped across lines is one hit and a call chained after it is out of reach (:86-:87,:131-:139); and only a whole dot-separatedTestingpart marks test-support code, so a production folder that merely contains the word is still scanned (:123-:129). - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; nothing constructs it. MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs).
CascadeSoftDeleteConventionTests, ConcurrencyConventionTests, ContractImplementationTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain· (see per-type table) · Level 13 · class
Three more thin subclasses of the same shape as the fifteen above, each supplying only Map. They are grouped separately because they share a second property that is worth understanding on its own: all three are ratchets rather than assertions in this repo. MMCA.Common has nothing today that could violate them, so a green run proves nothing about the framework's current code, and each subclass's own doc says so plainly.
- Depends on - the corresponding
*TestsBaseand CommonArchitectureMap, and nothing else. - Concept introduced - a deliberately vacuous gate, kept because of when it will first fire. The instinct is to delete a test that asserts nothing, and for a unit test that instinct is right. For a convention gate it is wrong, and the reason is timing. A convention is cheapest to hold on the first type that could break it, when the shape is still a draft and nobody depends on it; it is most expensive to retrofit later, across every type that has since copied the first one. Running the rule from the start means the invariant is enforced from that first type onward with no test for anyone to remember to write. The cost is that the suite carries three facts that currently read nothing, and the mitigation is that each carries its own note saying so, so a reader is never misled into thinking the framework has been checked. Compare PiiConventionTests, which is vacuous in the same way and is paired with a non-vacuous behavioral test, and ServiceContractPurityTests, which spells the ratchet argument out at length. [Rubric §34 - Architecture Governance & Documentation] is the category: a ratchet is governance expressed as code with a known, dated cost.
| Type | File:Line | Base rule | What it will enforce, and why it is vacuous today |
|---|---|---|---|
CascadeSoftDeleteConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14 |
CascadeSoftDeleteConventionTestsBase | An aggregate root that owns children deletes them in its own Delete() override. Supplies only Map (:16). The framework's Source/ declares no child-bearing aggregate today, so this run is the ratchet; it fires the moment the framework grows one whose children a delete would leave active. Its doc points at CascadeSoftDeleteFitnessTests as where the rule's own behavior is proven (:5-:12). [Rubric §8 - Data Architecture, Rubric §30 - Compliance/Privacy.] |
ConcurrencyConventionTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13 |
ConcurrencyConventionTestsBase | No *UpdateRequest carries a concurrency token in its body: the token belongs in the If-Match header, not in the payload, so a client cannot forge or omit it silently. Supplies only Map (:14). MMCA.Common is module-less, so there is no update request to judge; the rule fires if the framework itself grows one that reintroduces a body token (:5-:10). [Rubric §9 - API & Contract Design.] |
ContractImplementationTests |
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11 |
ContractImplementationTestsBase | The class serving a published [ServiceContract] interface must not itself be public, so consumers bind to the contract rather than to the implementation. Supplies only Map (:13). MMCA.Common marks no interface with the attribute today, so the rule is attribute-driven and currently selects nothing (:5-:9). Its purity sibling ServiceContractPurityTests is in the same position for the same reason. [Rubric §7 - Microservices Readiness, Rubric §1 - SOLID.] |
- Why they're built this way - each is a two-line subclass with a doc comment that is longer than its body, which is the correct ratio for a ratchet: the code says nothing surprising and the comment carries the whole decision. Keeping them in this repo also means the framework is held to the same bar it exports, so a new framework type that would break a consumer's gate fails here first rather than in the consumers after a release.
- Where they're used - all three run in the
MMCA.Common.Architecture.Testsproject during CI'sbuild-and-testjob. The same three bases are subclassed non-vacuously by MMCA.Store and MMCA.ADC over their own modules. - Caveats / not-in-source - a green run of any of these three says nothing about the framework's code today. Only
CascadeSoftDeleteConventionTestshas a companion that proves its rule fires at all (CascadeSoftDeleteFitnessTests); the other two rules have no fixture-backed proof in this repo.
DomainThrowFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DomainThrowFitnessTests.cs:17· Level 13 · class
- What it is - the meta-test for the domain-throw rule: ten facts pinning both offender kinds, the three permitted argument guards, the compiler-written switch-expression throw, the bare rethrow, the non-throwing control, the UNVERIFIABLE verdict, both allowlist granularities, and a final run of the rule against MMCA.Common's own Domain.
- Depends on - ArchitectureRules (
DomainThrowsOnlyArgumentGuards,DomainThrowFitnessTests.cs:41), its nested FixtureAssemblyMap (:150-:158), CommonArchitectureMap (:143), the fixture types inMMCA.Common.Architecture.Tests.DomainThrowFixtures(:1), includingSwitchExpressionFixture(DomainThrowFixtures.cs:115), andXunit.Sdk.XunitException(:3). - Concept introduced - the allowlist doing its day job, in the test's own setup. Every other self-test in this assembly points its map at a namespace whose only inhabitants are its fixtures. This one cannot: the rule reads the whole Domain-layer assembly, and this assembly contains throws that are not this file's fixtures. NavigationContractTests guards its embedded resource with an
InvalidOperationException, the OpenAPI XML-comment source generator emits a transformer into every assembly that references the API layer, and the substitutability rule's own fixtures (see CommonSubstitutabilityTests) throw on purpose. All three are allowlisted in a named static array with a doc comment explaining each (:21-:34), and the comment makes the wider point: generated plumbing is exactly what an allowlist is for. The third entry, the namespaceMMCA.Common.Architecture.Tests.SubstitutabilityFixtures(:33), is what keeps a sibling rule's deliberate offenders from leaking into this rule's report. The first entry is a fully qualified type name,MMCA.Common.Architecture.Tests.Ui.NavigationContractTests(:31), so the allowlist tracks the test's folder: moving that class underUi/moved the string with it. Reading that array teaches more about how the escape hatch is meant to be used than any of the escape-hatch facts do. The final fact is a different kind of assertion again:MMCACommonDomain_HoldsOnlyArgumentGuards(:140-:147) runs the rule against the real CommonArchitectureMap with an empty allowlist and assertsNotThrow, with thebecauserecording the fact that makes it interesting, that MMCA.Common's own Domain is the reference implementation of ADR-013 and its single throw is theArgumentNullExceptionguard inSpecification.cs. That is the framework holding itself to the rule it exports, with no exemptions at all. [Rubric §4 - DDD] and [Rubric §15 - Best Practices & Code Quality] are the properties; [Rubric §34 - Architecture Governance & Documentation] covers the allowlist-with-reasons discipline; [Rubric §14 - Testability] is the technique. - Walkthrough - a
private const string FixtureNamespace(:19), theNonFixtureThrowsarray (:29-:34) and a readonly FixtureAssemblyMap (:36) are shared; every fixture fact passesNonFixtureThrowsso the fixtures are the only subject.BusinessException_IsFlagged(:38-:47): asserts both the owning type and the exception's full nameSystem.InvalidOperationExceptionappear (:45-:46). Naming the exception is what tells a developer which throw to convert.CustomDomainException_IsAlsoFlagged(:49-:58): the custom-exception case.ArgumentGuards_AreNotFlagged(:60-:69): the permitted-guard case, with abecauselisting all three exception types and stating why they are exempt, a caller bug rather than a business outcome.CompilerSwitchExpressionThrow_IsNotFlagged(:71-:80): asserts the report never namesSwitchExpressionException. Its subject isSwitchExpressionFixture, a switch expression with no discard arm (DomainThrowFixtures.cs:111-:118), whose default-arm throw is written by the compiler rather than the developer, so the rule must not charge it to the domain method.BareRethrow_IsNotFlagged(:82-:91) andNonThrowingCode_IsNotFlagged(:93-:102): the two silent cases.ThrowOfAValueBuiltElsewhere_IsReportedAsUnverifiable(:104-:113): asserts the literal"UNVERIFIABLE"and the owning method's type name, the same third-verdict discipline ErrorCatalogFitnessTests pins for the catalog rules.AllowlistedNamespace_SilencesTheRule(:115-:124) andAllowlistedType_SilencesOnlyThatType(:126-:138): the two granularities, each built by spreadingNonFixtureThrowsand appending one entry, with the type-level fact carrying the control assertion that the other offender is still reported (:136-:137).MMCACommonDomain_HoldsOnlyArgumentGuards(:140-:147): the self-application described above.
- Why it's built this way - ten facts for one rule, and the reason is that this rule has the widest blast radius: it judges every
throwin every Domain assembly of every repo that adopts it, so both its false positives and its blind spots are expensive. Asserting the exception's full name rather than just the type's is what makes the report usable. See ADR-013 and ADR-015. - Where it's used - an independent class in the Common architecture suite; the shipped consumer-facing binding of the rule is DomainThrowTestsBase.
- Caveats / not-in-source - the
Microsoft.AspNetCore.OpenApi.Generatedallowlist entry (:32) silences a whole generated namespace. If the generator ever emitted a type a developer also wrote into that namespace, its throws would be silenced too; nothing in the class detects that. The sibling entry is a string, not atypeof, so renaming or relocatingNavigationContractTestswithout editing:31would quietly re-arm the rule against it. TheSubstitutabilityFixturesentry (:33) is also a string and silences everything under that namespace, so a non-fixture type added there would be exempt from the rule too.
CascadeSoftDeleteConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14· Level 13 · class
- What it is - the cascade-soft-delete convention binding for MMCA.Common's own map: an aggregate root that owns children must delete them in its own
Delete()override. - Depends on - CascadeSoftDeleteConventionTestsBase (
CascadeSoftDeleteConventionTests.cs:14) and CommonArchitectureMap (:16). - Concept - MMCA.Common's
Source/declares no child-bearing aggregate today, so this run is a ratchet rather than a live assertion: it does nothing until the framework itself grows an aggregate whose children a delete would leave active, at which point it starts failing. It is the same rule Store and ADC subclass over their own modules, where it does have live subjects. The rule's own detection logic is proven separately, against compiled fixtures, by CascadeSoftDeleteFitnessTests. - Walkthrough -
public sealed class CascadeSoftDeleteConventionTests : CascadeSoftDeleteConventionTestsBase(:14) overridesMapwithnew CommonArchitectureMap()(:16); it declares no other members. - Why it's built this way - a module-less framework has no positive case to assert, so shipping the binding anyway (rather than skipping it) is what turns "grows an aggregate with children" into a build failure instead of a silent gap.
- Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps, where the rule has real aggregates to check.
ClockReadTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:14· Level 13 · class
- What it is - both the framework's own binding of the ambient-clock rule and its meta-test: it subclasses ClockReadTestsBase over CommonArchitectureMap, so the inherited fact runs the rule against MMCA.Common's own Domain and Application, and it adds seven fixture facts pinning that a direct read of either clock type is flagged, that
DateTime.Todayis flagged, that a read inside a lambda or an async state machine is still attributed to the member that wrote it, that a read inside an async lambda is attributed to its source member and exempted by that member's allowlist entry, that an injectedTimeProvideris left alone, and both allowlist granularities (class doc,ClockReadTests.cs:7-:13). - Depends on - ClockReadTestsBase (
public sealed class ClockReadTests : ClockReadTestsBase,:14), CommonArchitectureMap (:20), ArchitectureRules (DomainAndApplicationDoNotReadTheClock,:88), its nestedFixtureAssemblyMap(:97-:106), and the eight fixtures in the siblingMMCA.Common.Architecture.Tests.ClockReadFixturesnamespace (:16): UtcNowReadingFixture, OffsetNowReadingFixture, LambdaClockReadingFixture, AsyncClockReadingFixture,AsyncLambdaClockReadingFixture, InjectedClockFixture, TwoMemberClockFixture andTodayReadingFixture(ClockReadFixtures.cs:48). - Concept introduced - a compiler-generated body still belongs to the member that wrote it. A lambda closure and an async state machine each compile into their own nested type, so a naive IL scan would attribute a clock read inside either to that generated type rather than to the source.
ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember(:37-:48) is the fact that pins the rule does not make that mistake:LambdaClockReadingFixture.ClockandAsyncClockReadingFixture.StampAsyncboth appear in the report under their declaring member's name, not a compiler-generated one. [Rubric §14 - Testability] is the technique, a fitness function proven against fixtures before it is trusted against real code; [Rubric §4 - DDD] is the property, since the rule is what lets a domain method take time as an input instead of reading it. - Walkthrough -
Mapis overridden withnew CommonArchitectureMap()(:20), which is all the inheritedDomainAndApplication_ShouldNotReadTheAmbientClockfact needs (ClockReadTestsBase.cs:26-:28). For the self-tests, aprivate const string FixtureNamespace(:16) and a readonly nestedFixtureAssemblyMapinstance (:18) point the rule at this test assembly's own Domain layer, anchored onInjectedClockFixture(:104); a privateReporthelper (:83-:95) runs the rule against that map and returns the caughtXunitException.Message, or an empty string when the rule passes. Because the fixture map registers the whole test assembly, every self-test asserts on what the report names rather than on the report being empty (:10-:12).DirectReadsOfEitherClockType_AreFlagged(:22-:29): asserts the report names bothUtcNowReadingFixture.Stamp(DateTime.UtcNow) andOffsetNowReadingFixture.Stamp(DateTimeOffset.Now).DateTimeToday_IsFlagged(:31-:35): assertsTodayReadingFixture.Stamp reads DateTime.Today, with thebecausethatDateTime.Todayreads the ambient clock exactly asDateTime.Nowdoes.ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMember(:37-:48): the attribution case above.ReadsInsideAnAsyncLambda_AreAttributedToTheSourceMemberAndExemptedWithIt(:50-:62): an async lambda compiles to a state machine nested in the closure's display class, so the member name must be read past both leading brackets; assertsAsyncLambdaClockReadingFixture.Clock reads DateTime.UtcNowis reported, and that allowlisting{FixtureNamespace}.AsyncLambdaClockReadingFixture.Clockremoves the fixture from the report, as the rule's doc promises.InjectedTimeProvider_IsNotFlagged(:64-:68): the control, assertingInjectedClockFixturenever appears.MemberAllowlistEntry_ExemptsOnlyThatMember(:70-:77): allowlistsTwoMemberClockFixture.Exemptedby its fully qualified member name and asserts the siblingStillReportedmember on the same type is still flagged, proving the allowlist grain works at member level, not type level.NamespaceAllowlistEntry_ExemptsEveryTypeUnderIt(:79-:81): allowlists the whole fixture namespace and asserts nothing from it appears.
- Why it's built this way - the rule inspects IL rather than source, so its riskiest cases, a closure and a state machine, need their own fixtures and their own fact; without
ReadsInsideLambdaAndAsyncBodies_AreFlaggedUnderTheirSourceMembera regression that misattributed either would pass every other fact silently.DateTime.Todaygets its own fixture for the same reason: it is a third property name on the clock type, and a rule that matched onlyNowandUtcNowwould miss it. The two allowlist facts follow the same member-vs-namespace split DomainThrowFitnessTests uses for its own allowlist. Folding the binding and the meta-test into one class means the framework runs the exact rule it ships against its own code in the same file that proves the rule works. - Where it's used - discovered and run by xUnit in the Common architecture suite. ADC's sibling binding of ClockReadTestsBase is
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs. See ADR-128.
CloudVendorIsolationTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17· Level 13 · class
- What it is - the framework's own binding of the cloud-vendor boundary: every layer of CommonArchitectureMap, the service defaults (
MMCA.Common.Aspire) and the test infrastructure packages must name no Azure, AWS or Google Cloud SDK type and reference no vendor SDK or vendor adapter assembly (class doc,CloudVendorIsolationTests.cs:5-:16; rule doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:3-:14). - Depends on -
CloudVendorIsolationTestsBase(public sealed class CloudVendorIsolationTests : CloudVendorIsolationTestsBase,:17; see the rollup), CommonArchitectureMap (:21), and four assembly anchors:Aspire.Extensions(:22), CrossServiceFixtureBase (:23),AppHostFixtureBasefromMMCA.Common.Testing.Aspire(:24) and the base type itself, which stands forMMCA.Common.Testing.Architecture(:25). - Concept introduced - the runtime half of a two-gate boundary. The compile-time guard,
EnforceCloudVendorBoundaryinSource/Build/MMCA.Common.LayerEnforcement.targets, judges declared package references; this class judges the compiled assemblies, so a vendor type that arrives through a transitive path is still caught (CloudVendorIsolationTestsBase.cs:9-:12). Unlike most bindings in this suite it does not take anIArchitectureMap: the base asks for a flatCloudNeutralAssemblieslist (CloudVendorIsolationTestsBase.cs:18), because the boundary covers packages (Aspire, the testing packages) that are not layers of any map. [Rubric §34 - Architecture Governance & Documentation] is the category; [Rubric §14 - Testability] the technique. - Walkthrough - two overrides, no facts of its own.
CloudNeutralAssemblies(:19-:26): a collection expression spreadingnew CommonArchitectureMap().Layers.Select(layer => layer.Assembly)(:21) and appending the four anchor assemblies above (:22-:25), which the class doc calls the places the Azure dependencies used to live (:8).AllowedVendorPrefixes(:28-:32): an ordinal-keyed dictionary with one entry,MMCA.Common.Infrastructuremay nameMicrosoft.Azure.Cosmos(:31), because Cosmos DB is one of the four EF engines rather than a cloud service a host opts into (:12-:14). The base default is empty (CloudVendorIsolationTestsBase.cs:24-:25).- The two inherited facts,
CloudNeutralAssemblies_ShouldNotName_ACloudVendorTypeandCloudNeutralAssemblies_ShouldNotReference_ACloudVendorAssembly(CloudVendorIsolationTestsBase.cs:27-:33), callArchitectureRules.CloudNeutralAssembliesNameNoVendorTypeandArchitectureRules.CloudNeutralAssembliesReferenceNoVendorAssembly(see ArchitectureRules) with both overrides.
- Why it's built this way - the allowance is scoped per assembly name, not global, so the Cosmos exemption cannot leak into any other cloud-neutral assembly. Keeping the rule body in the shipped base means a consumer subclasses it with its own assembly list. See ADR-134.
- Where it's used - discovered and run by xUnit in the Common architecture suite; nothing constructs it.
MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudVendorRulesTests.cs:13names it in a doc comment as the positive half of the vendor rules, whose negative half that class proves against Google SDK fixtures.
CommonSubstitutabilityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance.Substitutability·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/CommonSubstitutabilityTests.cs:10· Level 13 · class
- What it is - the substitutability binding for MMCA.Common's own map: no override or interface implementation may refuse the contract it fulfils by throwing
NotSupportedExceptionorNotImplementedException. - Depends on - SubstitutabilityTestsBase (
CommonSubstitutabilityTests.cs:10; its one fact isOverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented,SubstitutabilityTestsBase.cs:34), IArchitectureMap and CommonArchitectureMap (:12). - Concept introduced - Liskov substitution as a build gate. A subtype that refuses an operation its base or interface promises breaks every caller that substitutes it in, and only at run time. The base reads IL with the same
throwscan as DomainThrowTestsBase, so only an exception constructed in place and thrown is judged (SubstitutabilityTestsBase.cs:10-:14). The subclass supplies onlyMap(:12); the base's protectedExcludedMembers(SubstitutabilityTestsBase.cs:31, empty by default) is the reviewed-refusal escape hatch and this binding does not override it, so the framework runs with no exemptions. [Rubric §1 - SOLID] is the property and [Rubric §34 - Architecture Governance & Documentation] the category. - Why it's built this way - a one-line binding keeps the rule body once in the shipped package, so Store and ADC subclass the same base over their module assemblies. The rule's own fixtures live under
MMCA.Common.Architecture.Tests.SubstitutabilityFixturesand throw deliberately, which is why DomainThrowFitnessTests allowlists that namespace (DomainThrowFitnessTests.cs:33). See ADR-015. - Where it's used - run by the
MMCA.Common.Architecture.Testsproject in CI'sbuild-and-testjob; nothing references it from code. - Caveats / not-in-source - the fixtures that prove the rule fires are not part of this unit's source slice, so whether a companion self-test exists is Not determinable from the brief.
ConcurrencyConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13· Level 13 · class
- What it is - the optimistic-concurrency convention binding for MMCA.Common's own map: no
*UpdateRequestcarries a concurrency token in its request body. - Depends on - ConcurrencyConventionTestsBase (
ConcurrencyConventionTests.cs:13) and CommonArchitectureMap (:14). - Concept - MMCA.Common is module-less, so this run is a ratchet rather than a live assertion, the same shape as CascadeSoftDeleteConventionTests: it does nothing until the framework itself grows an update request that reintroduces a body token, and it is the same rule Store and ADC subclass over their own modules.
- Walkthrough -
public sealed class ConcurrencyConventionTests : ConcurrencyConventionTestsBase(:12) overridesMapwithnew CommonArchitectureMap()(:14); it declares no other members. - Why it's built this way - keeping the binding shipped even with no live subject means the convention starts failing the moment it is violated, rather than only after someone remembers to add a check for it.
- Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
ContractImplementationTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11· Level 13 · class
- What it is - the
[ServiceContract]encapsulation rule binding for MMCA.Common's own map: the class serving a published contract interface must not itself be public. - Depends on - ContractImplementationTestsBase (
ContractImplementationTests.cs:11) and CommonArchitectureMap (:13). - Concept - MMCA.Common marks no interface
[ServiceContract]today, so this run is a ratchet rather than a live assertion, the same shape as CascadeSoftDeleteConventionTests and ConcurrencyConventionTests. The rule is attribute-driven rather than name-pattern-driven; the reasoning for that choice lives on the shared base rather than being repeated here. - Walkthrough -
public sealed class ContractImplementationTests : ContractImplementationTestsBase(:11) overridesMapwithnew CommonArchitectureMap()(:13); it declares no other members. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass in this suite uses (ADR-015). - Where it's used - an independent class in the Common architecture suite; Store and ADC subclass the same base with their own maps.
EntityConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain.EntityModel·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/EntityConventionTests.cs:12· Level 13 · class
- What it is - the framework repo's binding of the DDD entity-shape rules: MMCA.Common's own Notifications aggregates are sealed, built through a
Result-returningCreatefactory with no public constructor, and expose no public setter (class doc,EntityConventionTests.cs:5-:11). - Depends on - EntityConventionTestsBase (
:12) and FrameworkModuleArchitectureMap (:14), not CommonArchitectureMap. - Concept - the map choice is the whole point. The base's facts read the module-scoped accessors, so over CommonArchitectureMap they would have nothing to judge and pass vacuously; FrameworkModuleArchitectureMap registers the framework assemblies as a module so the rules land on PushNotification and UserNotification (
:9-:10). [Rubric §4 - DDD] assesses whether the tactical patterns are actually applied; this is what makes the framework obey the factory-plus-Resultconstruction contract it asks consumers to follow (ADR-129). - Walkthrough - one member,
Map(:14). Eight inherited facts (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15,:18,:21,:24,:27,:30,:33,:36) cover aggregate-root exposure, theResult-returningCreatefactory, no public aggregate constructors, factory return types, sealing, Domain-layer placement, non-public property setters, and the DTO/request exclusion from Domain and Infrastructure. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass in this suite uses (ADR-015), with the map swapped so the rules are live rather than ratchets. - Where it's used - discovered and run by xUnit in the Common architecture suite; its sibling ImmutabilityTests uses the same map. ADC subclasses the same base with its own map (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs).
DeleteBehaviorConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs:28· Level 14 · class
- What it is - the framework repo's binding of the delete-behavior fitness functions (ADR-119): a sealed subclass that builds the framework's own two finalized EF Core models (SQLite and SQL Server) with no database connection opened, and asserts each against the
RestrictDeleteByDefaultConventionstamp the model already carries. - Depends on - DeleteBehaviorConventionTestsBase (
public sealed class DeleteBehaviorConventionTests : DeleteBehaviorConventionTestsBase,DeleteBehaviorConventionTests.cs:28), ArchitectureRules (CascadingForeignKeysAreExplicitlyOptedIn,ConventionForeignKeysRestrictDeletes,DeleteBehaviorConventionTests.cs:57,:66),SqliteDbContextandSQLServerDbContext(:79,:94-:98), and a set of private no-op fakes (NoModuleAssemblies,NoEntityDataSources,NoDomainEventDispatcher,NoOutboxSignal,:122-:163) that satisfy the DI graphApplicationDbContext.OnConfiguringdemands without any real registrations. - Concept - the base class's own
Modelproperty only exercises one engine, so this subclass adds a second[Fact](SqlServerModel_CascadingDeletes_AreExplicitlyOptedIn/SqlServerModel_ConventionDeletes_AreRestricted,:53-:69) that builds a second model againstSQLServerDbContextand asserts both rules against it too, covering both relational engines the framework maps entities onto today. The nestedFrameworkModelshelper documents the trick that makes this possible without infrastructure: only the model is asserted, so neither context ever opens its connection, and EF builds the model from the configurations alone (:71-:74).NoEntityDataSourcesanswersTryGetDataSourceKeywithfalsefor everything so the cross-source degrade convention becomes a no-op, and its two other lookups return a default key rather than throwing so a throwing fixture does not trip the repo's own domain-throw rule (:127-:131). - Walkthrough -
Model(inherited) builds the SQLite model viaDbContextOptionsBuilder<SqliteDbContext>().UseSqlite("DataSource=:memory:")andFrameworkModels.Sqlite()(:77-:90); the two added facts build the SQL Server model the same way viaFrameworkModels.SqlServer()(:92-:105). Both paths construct their context with the same four dependencies:Services()(anAuditSaveChangesInterceptor, aDomainEventSaveChangesInterceptor, and a no-opIEntityDataSourceRegistry,:108-:118), aNoModuleAssembliesprovider that returns an empty assembly list so the model holds only the framework's own tables (:121-:125), and aPhysicalDataSourcedescribing the (unused) connection. Both inherited facts then readcontext.Modeland assert the convention's stamps against it. - Why it's built this way - PostgreSQL takes the SQL Server mapping unchanged for delete-behavior purposes, so asserting SQLite plus SQL Server covers the framework's relational engines without a third context class; Cosmos is a deliberate no-op for this convention since it has no foreign key constraints to restrict (
DeleteBehaviorConventionTestsBase.cs:16-:18). Building the model with in-memory/unopened connections keeps the fitness test fast and database-free, consistent with the rest ofMMCA.Common.Architecture.Tests. - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; nothing constructs it.MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.csis the sibling binding in the ADC repo, built the same way against ADC's own module assemblies.
EventNameConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventNameConventionTests.cs:13· Level 13 · class
- What it is - the meta-test for the
[EventName]rule: three facts pinning the offender, the compliant control and the framework's own events. - Depends on - ArchitectureRules (
IntegrationEventsDeclareEventName,ArchitectureRules.Events.cs:33), CommonArchitectureMap (EventNameConventionTests.cs:37-:40), its nestedFixtureProbeMap(:43-:49, see the rollup), the fixture events inMMCA.Common.Architecture.Tests.EventNameFixtures.IntegrationEvents(FixtureEventNameMissingEvent,FixtureEventNameDeclaredEvent;EventNameFixtures.cs:1) andSystem.Exception. - Concept introduced - pinning a persistence-safety rule to its reason. The rule requires every concrete integration event to declare
[EventName], so outbox and inbox rows store a stable wire identity rather than a CLR type name that a rename, namespace move or assembly move would orphan (ArchitectureRules.Events.cs:27-:41). The test'sbecausestrings repeat that reason (:22), and the report assertion"[EventName"(:23) checks the message tells the developer what to add. [Rubric §9 - API & Contract Design] and [Rubric §14 - Testability] apply. - Walkthrough -
FixtureProbeMapis a module-less map over this test assembly (:43-:49), its singleSharedlayer built fromtypeof(FixtureEventNameMissingEvent).Assembly(:48), so every fixture event is scanned.EventWithoutEventName_IsFlagged(:16-:24): the report namesFixtureEventNameMissingEventand contains"[EventName".EventWithEventName_IsNotFlagged(:27-:35): the compliant fixture is absent from the report.FrameworkEvents_AllDeclareEventName(:38-:40): runs the rule against the realCommonArchitectureMapand assertsNotThrow, the framework holding itself to the rule it ships.
- Why it's built this way - the first two facts assert on the failure message because the rule reports by throwing; a flagged and an unflagged control in one assembly is what proves the rule discriminates. The consumer-facing binding is the
IntegrationEvents_ShouldDeclare_EventNamefact inEventConventionTestsBase(EventConventionTestsBase.cs:21). See ADR-015. - Where it's used - an independent class in the Common architecture suite; fixtures sit in
EventNameFixtures.cs. - Caveats / not-in-source - the attribute is matched by full name and must sit on the event itself (it is not inherited,
ArchitectureRules.Events.cs:29-:31), so a derived event relying on its base's[EventName]is flagged.
EventScopeFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:13· Level 13 · class
- What it is - the ownership-scoping guard for the integration-event rules: three facts pinning that a consumer-shaped map neither snapshots nor polices the framework's own events, while the framework's module-less map still covers them at the source.
- Depends on - ArchitectureRules (
BuildIntegrationEventContractandIntegrationEventsResideInSharedIntegrationEventsNamespace,EventScopeFitnessTests.cs:18,:30), CommonArchitectureMap (:39), its own FakeConsumerMap, and BaseIntegrationEvent as the Domain-assembly anchor (:1,:56). - Concept introduced - a rule's scope is part of its contract, and ownership decides it. Both event rules ask the same question of a map, and both answer differently depending on whether the map declares modules.
IntegrationEventsincludes framework layers only whenmap.ModuleNames.Count == 0(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:155-:73), and the residency rule enforces the Shared-layer requirement only when the map is module-bearing (:31,:33). The reasoning is ownership: a framework-shipped event is the framework's contract, gated by the framework's own conventions and public-API baseline, so a consumer's frozen snapshot must neither churn on it nor claim jurisdiction over where it lives. This class is a regression guard, and the class doc names the incident that motivated it (:6-:12): when the framework shipped its first concrete integration event, OutputCacheEvictionRequested, the Helpdesk canary broke. [Rubric §6 - CQRS & Event-Driven] assesses whether event contracts are governed; [Rubric §9 - API & Contract Design] covers the frozen-snapshot mechanism; [Rubric §33 - Developer Experience] is the failure mode being prevented, a framework release that reds every consumer's architecture suite for a change they did not make. - Walkthrough
ModuleBearingMap_ExcludesFrameworkEvents_FromContractSnapshot(:15-:23): builds the contract from FakeConsumerMap and asserts no line mentionsOutputCacheEvictionRequested.ModuleBearingMap_PassesResidencyRule_DespiteFrameworkEvents(:26-:34): asserts the residency rule does not throw for the consumer map, even though the framework's Domain assembly hosts that event outside any Shared assembly (inline comment,:28-:29).ModuleLessMap_StillCoversFrameworkEvents(:36-:44): the other half of the proof, using the real CommonArchitectureMap and asserting the same event is in the contract. Without this third fact the scoping change would be indistinguishable from simply having disabled the rule.
- Why it's built this way - a scoping fix is exactly the kind of change that silently over-corrects. Pinning both the exclusion and the retention, against a real framework event rather than a fixture one, is what keeps the fix from becoming a hole.
- Where it's used - an independent class in the Common architecture suite; the rules it scopes are bound for real by
EventVersioningConventionTestshere and by the per-repo IntegrationEventContractTestsBase subclasses in the consumers.
EventUpcasterFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:12· Level 13 · class
- What it is - the meta-test for the two event-upcaster fitness rules: five facts proving the unique-source rule reports a contract claimed twice, that it leaves a legitimate chain alone, that it rejects one class declaring two upcaster contracts, that the version rule reports an upcaster pointing at a lower
SchemaVersion, and that both rules pass on a map owning no upcasters at all. - Depends on - ArchitectureRules (
EventUpcastersHaveUniqueSourceTypesandEventUpcastersIncreaseSchemaVersion,EventUpcasterFitnessTests.cs:64,:66,:72,:79), the fixture contracts and fixture upcasters plusFixtureDoubleHopUpcaster(see the rollup) (:1), its nested UpcasterTestMap (:85-:91), CommonArchitectureMap (:62), plus xUnit and AwesomeAssertions. - Concept introduced - proving a rule is not vacuous when the framework itself has nothing to judge. MMCA.Common ships no upcaster of its own, so running either rule over the real CommonArchitectureMap proves only that it does not crash. That is a real property worth pinning (a rule that threw on an empty set would red every consumer that has not adopted upcasting yet), and the last fact pins exactly it. But the other four facts have to manufacture their subjects, which is what the
UpcasterFixturesnamespace and the private map are for. The pattern to take away: a fitness function for a consumer-facing convention is proven in the framework repo against fixtures, and only smoke-tested against the framework's own code. ProtoContractFitnessTests, CommandValidatorCoverageFitnessTests and ServiceContractPurityTests sit in the same position for their rules. [Rubric §6 - CQRS & Event-Driven] and [Rubric §9 - API & Contract Design] are the properties defended; [Rubric §14 - Testability] is the technique. - Walkthrough - two private helpers each run one rule against a fresh UpcasterTestMap and return the thrown message:
RunUniqueSourceRule(:70-:75) andRunSchemaVersionRule(:77-:82). Because the fixtures always contain an offender for each rule, both helpers can assertShould().Throw<Exception>().Which.Messageunconditionally and let the facts make positive and negativeContainassertions against the one string.UniqueSourceRule_FlagsTheContestedContract_ButNotTheCompliantLadder(:14-:25): asserts the message names the contested contract and both rival upcasters (:19-:21), which is what makes the failure actionable, and that the compliant first rung is absent (:22-:24).UniqueSourceRule_DoesNotFlag_AnUpcasterWhoseSourceIsAnotherUpcastersTarget(:31-:33): the sharp one, given its own fact and its own doc comment (:27-:30). The compliant ladder's middle contract is both a target and a source; that is a chain, not a duplicate claim, and the rule must leave it alone.UniqueSourceRule_FlagsAClassDeclaringTwoUpcasterContracts(:37-:44): asserts the message namesFixtureDoubleHopUpcaster(:42) and contains"exactly one hop"(:43). The comment above it gives the reason (:35-:36): the registry indexes oneSourceTypeper upcaster instance, so a class declaring twoIEventUpcaster<,>contracts is one hop at runtime and must be rejected outright; the rule's report line spells out the same reason (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:23).SchemaVersionRule_FlagsTheBackwardsUpcaster_ButNotTheCompliantLadder(:46-:57): names the offender (:51) and, notably, pins the message text"must declare a HIGHER SchemaVersion"(:52-:54), because the wording is what tells a developer which direction is allowed. Both clean rungs are asserted absent (:55-:56).BothRules_Pass_OnAMapThatOwnsNoUpcasters(:59-:68): runs both rules against the real framework map and assertsNotThrow, with thebecausestating that the framework ships no upcaster so the rule passes vacuously (:65).
- Why it's built this way - the two rules exist because an upcast chain that is not a function, or that runs backwards, produces a bug that only appears when an old outbox row is replayed, potentially long after the change that caused it (ADR-090, building on ADR-010). Catching it at build time is worth a fixture namespace.
- Where it's used - an independent class in the Common architecture suite. The shipped binding of both rules is EventConventionTestsBase, whose two facts call them for every repo (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:27,:30), and which MMCA.Common activates through EventVersioningConventionTests.
FakeConsumerMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventScopeFitnessTests.cs:50· Level 13 · class
- What it is - a consumer-shaped architecture map: the framework's Domain assembly registered as a framework layer plus one module layer, which is the minimum that makes a map module-bearing.
- Depends on - ArchitectureMapBase (
private sealed class FakeConsumerMap : ArchitectureMapBase,EventScopeFitnessTests.cs:50), LayerRef, the Layer enum, and BaseIntegrationEvent as the anchor for the framework Domain assembly (:56). - Concept introduced - the
Module(...)factory, and what a single module entry changes. Every other map in this chapter uses onlyFramework(...), whoseLayerRef.Moduleis the empty string (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:94-:95).Module(name, layer, assembly)sets it to the module name and derives the root namespace as{RepoToken}.{module}.{segment}(:98-:99), andModuleNamesis computed from exactly those non-empty entries (:27-:32). One module layer is therefore all it takes to flip a map from "framework" to "consumer" for every rule that branches on ownership. Building that shape from the framework's own assemblies, rather than referencing a real consumer, keeps the regression guard inside MMCA.Common's CI where it can run before a release. - Walkthrough -
RepoToken => "MMCA.FakeConsumer"(:52), deliberately notMMCA.Common, so the derived module namespace looks like a consumer's.DefineLayers()(:54-:58) is ayield-based iterator returning two entries:Framework(Layer.Domain, typeof(BaseIntegrationEvent).Assembly)(:56), which brings the framework's real integration event into the map, andModule("Fake", Layer.Shared, typeof(EventScopeFitnessTests).Assembly)(:57), a stand-in module Shared layer that ships no integration events of its own (class doc,:46-:49). That combination is the exact situation the guard exists for: a consumer whose map can see a framework event but does not own it. - Where it's used - the input to the first two facts of EventScopeFitnessTests (
:18,:31). - Caveats / not-in-source - the class doc says the stand-in module Shared layer ships no integration events (
:46-:49), but the assembly it points at is this whole test assembly, which does declare the eight upcaster fixture contracts. Those fixtures live in a*.IntegrationEventsnamespace precisely so the residency fact still passes (EventUpcasterFixtures.cs:9-:13), and the contract fact only asserts the absence of the framework's event, so neither fact is affected. The doc comment is narrower than the code.
FeatureFlagLifecycleRuleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:23· Level 13 · class
- What it is - the meta-test for the two feature-flag lifecycle rules: seven facts pinning the four ways a flag's declaration can be wrong, that a correctly declared flag is left alone, that a temporary flag past its
RemoveByfails naming the date and the owner, and that the rule passes both on the day of expiry and against the framework's own flags. - Depends on -
ArchitectureRules.FeatureFlagsDeclareLifetimeandArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy(FeatureFlagLifecycleTests.cs:97,:123), FeatureFlagAttribute as the taught convention (ADR-031, ArchitectureMapBase as the private FixtureMap's base,FeatureFlagLifecycleTests.cs:129), CommonArchitectureMap (:118), and the fixturesFixtureBadFeatures,FixtureGoodFeatures, andFixtureExpiredFeaturesin the siblingFeatureFlagFixturesnamespace (:1). - Concept introduced - a lifecycle rule is checked twice: once at declaration time, once at expiry time.
ArchitectureRules.FeatureFlagsDeclareLifetimeis the shape check: every flag constant must carry a[FeatureFlag]attribute, aPermanentflag must not setRemoveBy, and aTemporaryone must set it as a parseable ISOyyyy-MM-dddate.ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveByis the clock check: it takes aDateOnlyand fails any temporary flag whose date has passed, naming the flag, the date, and the owner in the message so the failure is actionable rather than just red. FramingTodayas a parameter rather than readingDateTime.UtcNowdirectly is what letsExpiryRule_PassesOnTheDayOfTheRemoveByDate(:110-:113) pin the boundary exactly: a flag due to be gone BY itsRemoveBydate is still inside its window on that date, not already expired. This is the dead-toggle detector the CQRS decorator pipeline'sFeatureGatestep depends on staying honest (MMCA.Common/CLAUDE.md, Feature Flag Lifecycle). [Rubric §34 - Architecture Governance & Documentation] is the property (a lifecycle contract enforced at build time, not by convention); [Rubric §14 - Testability] is the technique. - Walkthrough - a
private static readonly DateOnly Today = new(2026, 9, 11)(:50) freezes the clock so the expiry facts are deterministic; a nested FixtureMap (:129-:135) points a Shared-layer map at this test assembly so the rules see the deliberately bad fixtures.DeclarationRule_FlagsAConstantWithNoAttribute(:52-:56): asserts the failure message namesFixtureBadFeatures.UnannotatedFlag.DeclarationRule_FlagsAPermanentFlagThatSetsRemoveBy(:58-:65) andDeclarationRule_FlagsATemporaryFlagWithNoRemoveBy(:67-:74): the two ways a flag's lifetime and itsRemoveBydisagree, each asserting the flag name plus the specific wording ("is Permanent but sets RemoveBy", "must set RemoveBy").DeclarationRule_FlagsATemporaryFlagWhoseRemoveByDoesNotParse(:76-:83): the malformed-date case, asserting "is not an ISO yyyy-MM-dd date".DeclarationRule_LeavesCorrectlyDeclaredFlagsAlone(:85-:92): the control, assertingFixtureGoodFeatures.PermanentFlagandFixtureGoodFeatures.LiveTemporaryFlagare absent from the message.ExpiryRule_FlagsATemporaryFlagPastItsRemoveBy_NamingTheDateAndTheOwner(:94-:107): runs the expiry rule againstFixtureExpiredFeatures.ExpiredFlag, asserting the message names the flag, the date2020-01-01, andowner=Fixtures, and that the still-liveFixtureGoodFeatures.LiveTemporaryFlagis absent.ExpiryRule_PassesOnTheDayOfTheRemoveByDate(:109-:113): the boundary case above.ExpiryRule_PassesOnTheFrameworksOwnFlags(:115-:119): runs the expiry rule against the real CommonArchitectureMap atTodayand assertsNotThrow, with thebecauserecording that every framework flag isPermanentso none can expire, the same self-application pattern DomainThrowFitnessTests uses.
- Why it's built this way - the declaration rule alone would still let a temporary flag rot forever once its rollout finishes; pairing it with an expiry rule that runs against a caller-supplied
Todayis what turns "flag documented" into "flag that fails the build when it overstays". FreezingTodayas a class constant, rather than each fact computing its own offset fromDateTime.UtcNow, keeps every fact's expected message exact instead of relative. See ADR-031. - Where it's used - an independent class in the Common architecture suite; the shipped binding of both rules for a real repo is FeatureFlagLifecycleTests.
FeatureFlagLifecycleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs:13· Level 13 · class
- What it is - the two-line binding of the feature-flag lifecycle rules to MMCA.Common's own map: every flag MMCA.Common declares must carry a lifetime, and no temporary one may be past its
RemoveBy. - Depends on - FeatureFlagLifecycleTestsBase (
FeatureFlagLifecycleTests.cs:13) and CommonArchitectureMap (:15). - Concept introduced - none new; the same shipped-binding shape as AiDependencyIsolationTests above and every other
*TestsBasesubclass in this suite. The rules themselves are taught by FeatureFlagLifecycleRuleTests against fixtures; this class is what proves the framework's own flags actually pass them, not just that the rules can be called. - Walkthrough - overrides
Mapwithnew CommonArchitectureMap()(:15); the two[Fact]s live entirely in the base class and runArchitectureRules.FeatureFlagsDeclareLifetimeandArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy(againstDateOnly.FromDateTime(DateTime.UtcNow), the base's defaultToday) overCommonArchitectureMap. - Why it's built this way - the same one-class-per-repo pattern every other
*TestsBasesubclass follows (ADR-015): the rule body and every edge case live once in FeatureFlagLifecycleTestsBase / FeatureFlagLifecycleRuleTests, and each repo supplies only its ownIArchitectureMap. Because this fact runs against the live clock rather than a frozenToday, a framework flag that MMCA.Common itself lets overstay itsRemoveByfails this CI job on the day it expires, not only when someone thinks to check. - Where it's used - an independent class in the Common architecture suite; MMCA.ADC subclasses the same base with its own map (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs), and theFeatureFlagFixtures.csfile in this assembly feeds FeatureFlagLifecycleRuleTests its deliberately bad and good fixtures.
FixtureFormsTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:38· Level 13 · class
- What it is - a private, primary-constructor subclass of FormsConventionTestsBase nested in
FormsConventionRequirednessFitnessTests(see the rollup) that points the base's create-form scan at a fixture folder instead of a repo'sSource/Modules. - Depends on - FormsConventionTestsBase (
private sealed class FixtureFormsTests(string modulesRoot) : FormsConventionTestsBase,FormsConventionRequirednessFitnessTests.cs:38), IArchitectureMap and CommonArchitectureMap (:40). - Concept introduced - redirecting a file-scanning fitness base at fixtures through one virtual root. The base's
ModulesRootdefaults to{repo root}/Source/Modules, located fromMap.RepoToken(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:59-:60), and its doc says to override it to point the facts at a fixture tree (:55-:58). This subclass is that override. Because it is private, xUnit does not discover it, so an offending fixture does not fail the run; the outer class invokes the inherited fact directly and asserts on the outcome (class doc,FormsConventionRequirednessFitnessTests.cs:6-:11). [Rubric §14 - Testability] is the technique. - Walkthrough - two members.
Mapis a get-only auto-property initialized tonew CommonArchitectureMap()(:40), satisfying the base's abstractMap(FormsConventionTestsBase.cs:46);ModulesRootreturns the constructor'smodulesRoot(:42). The outer class builds the fixture root underTests/Architecture/MMCA.Common.Architecture.Tests/RazorFixtures/Forms(:14-:20) and drives the inheritedAdminCreateForms_ReadRequirednessOffTheirModelfact (FormsConventionTestsBase.cs:179-:203) twice:ACommentedOutRequirednessMarker_DoesNotSatisfyTheGate(:22-:28): over theCommentedfolder, asserts anXunitExceptionwhose message containsWidgetCreate.razor, proving aModelValidation.IsRequiredmarker inside a comment is stripped before the check (FormsConventionTestsBase.cs:187,:192).ALiveMarkerInTheSiblingFieldComponent_SatisfiesTheGate(:30-:36): over theCleanfolder, assertsNotThrow, proving a live marker in a sibling*FormFields.razorcomponent counts (FormsConventionTestsBase.cs:189-:193).
- Why it's built this way - the requiredness fact scans markup text, so the regression it guards (a commented-out marker satisfying it) can only be proven with real files on disk. Overriding one virtual path, rather than copying the scan into a test, keeps the meta-test running the exact shipped fact.
- Where it's used - constructed only by the two facts of its enclosing
FormsConventionRequirednessFitnessTests(:25,:33); nothing outside the defining file references it.
FrameworkConstructorDependencyTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs.ConstructorDependencies·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencies/FrameworkConstructorDependencyTests.cs:25· Level 13 · class (public, sealed)
- What it is - the single-responsibility ceiling applied to the framework itself: the three constructor-dependency facts of ConstructorDependencyCountTestsBase, re-pointed from the (empty) module populations at the framework's own Application, Infrastructure and API assemblies, with a ceiling of 7 for services, 5 for controllers and 7 for handlers (
FrameworkConstructorDependencyTests.cs:33,:37,:40). - Depends on - ConstructorDependencyCountTestsBase (
:25), CommonArchitectureMap (:31), the Layer enum and IArchitectureMap'sOfLayer(:43,:46,:49), plus reflection overControllerBaseand theICommandHandler/IQueryHandlercontracts, matched by full name rather than by type reference (:27-:29). - Concept introduced - reusing a consumer-shaped rule on a repo with no modules by overriding its populations, not its rule. The base's defaults scan module assemblies (
ScannedHandlers => Scan(Map.ModuleApplication(), IsCommandOrQueryHandler),MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:81), which are empty in MMCA.Common, so a plain two-line binding would pass vacuously. The class doc names the fix (FrameworkConstructorDependencyTests.cs:6-:12): override the three virtual populations (ConstructorDependencyCountTestsBase.cs:64,:71,:78) and the measured-constructor selector (:86) so the same facts judge the framework's own classes. Two choices make that honest. Abstract classes are included and protected constructors are measured (FrameworkConstructorDependencyTests.cs:51-:53), because the framework's widest classes are abstract bases such asAuthenticationServiceBaseand the*HandlerBase/*ControllerBasefamilies whose only constructors are protected (:10-:12). Optional parameters count, since an optional collaborator is still a collaborator (:22). [Rubric §1 - SOLID] is the property; [Rubric §34 - Architecture Governance & Documentation] is the ratchet discipline the doc states outright: lower a ceiling as remediation lands, never raise one to turn a run green (:23). - Walkthrough
- Ceilings:
MaxConstructorDependencies => 7(:33);MaxControllerConstructorDependencies => 5(:37), commented as the high-water mark ofCrudEntityControllerBase,InboxControllerandUserAccountAuthControllerBasewhen the gate landed (:35-:36);MaxHandlerConstructorDependencies => 7(:40), the mark ofResetPasswordHandlerBase(:39). These override the abstract members atConstructorDependencyCountTestsBase.cs:43,:47,:55. - Populations:
ScannedServicesis every framework class in the Application and Infrastructure layers (FrameworkConstructorDependencyTests.cs:42-:43);ScannedControllersis the API layer filtered byIsController(:45-:46);ScannedHandlersis the Application layer filtered byIsCommandOrQueryHandler(:48-:49), which per the class doc includes the decorators (:18-:20). MeasuredConstructors(:51-:53): public, protected, and protected-internal instance constructors.FrameworkClasses(:61-:68): distinct assemblies, top-level non-static classes, not compiler-generated, and not records (detected by the compiler-emitted<Clone>$method), because a positional record's parameters are data fields rather than injected collaborators (:55-:60).IsController(:70-:81) walks the base-type chain forMicrosoft.AspNetCore.Mvc.ControllerBase;IsCommandOrQueryHandler(:83-:87) matches any generic interface whose definition's full name is the open genericICommandHandler<,>orIQueryHandler<,>(arity 2).- The three facts themselves live in the base:
ApplicationServices_DoNotExceedConstructorDependencyCeiling,Controllers_DoNotExceedConstructorDependencyCeilingandHandlers_DoNotExceedConstructorDependencyCeiling(ConstructorDependencyCountTestsBase.cs:96,:116,:140).
- Ceilings:
- Why it's built this way - matching the handler and controller contracts by full-name string keeps the test assembly free of a compile-time reference it would only need for a type check, and keeping the rule body in the base means the framework and every consumer are judged by the same arithmetic; only the populations and the ceilings differ.
- Where it's used - an independent class in the Common architecture suite (no usage sites outside the defining file); MMCA.ADC binds the same base with its module populations through ConstructorDependencyCountTests.
ImmutabilityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain.EntityModel·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/ImmutabilityTests.cs:11· Level 13 · class (public, sealed)
- What it is - the MMCA.Common binding of the immutability rules: DTOs, command/query messages, domain events, integration events and value objects expose no public mutable setter, run over the framework's own assemblies (class doc,
ImmutabilityTests.cs:5-:10). - Depends on - ImmutabilityTestsBase (
:11) and FrameworkModuleArchitectureMap (:13), deliberately not CommonArchitectureMap. - Concept introduced - choosing the map that makes a module-scoped rule bite. The DTO, command/query and domain-event immutability rules scope to the map's module layers (
FrameworkModuleArchitectureMap.cs:6-:10), and CommonArchitectureMap registers the framework assemblies as framework layers, where those rules are vacuous.FrameworkModuleArchitectureMapregisters the same Shared, Domain and Application assemblies as one module namedFramework, leaving Infrastructure a framework layer (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/FrameworkModuleArchitectureMap.cs:19,:25-:28), so the rules actually run over the aggregates MMCA.Common ships, the Notifications family (:6-:11). It is the opposite move to FakeConsumerMap: there a module entry is added to prove a rule stays out of framework code; here it is added to pull framework code in. [Rubric §4 - DDD] is the property (the class doc tags rubric section 4,ImmutabilityTests.cs:6); [Rubric §34 - Architecture Governance & Documentation] is applying a consumer convention to the framework itself. - Walkthrough - one member,
Map(:13), overridden tonew FrameworkModuleArchitectureMap(). The five facts are inherited:Dtos_ShouldBe_Immutable,CommandsAndQueries_ShouldBe_Immutable,DomainEvents_ShouldBe_Immutable,IntegrationEvents_ShouldBe_ImmutableandValueObjects_ShouldBe_ImmutableSealedAndInShared(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13,:16,:19,:22,:25). - Why it's built this way - a two-line binding over the wrong map would be green forever and prove nothing; a dedicated map keeps the shared rule untouched while giving it real subjects in the repo that defines it.
- Where it's used - an independent class in the Common architecture suite. MMCA.ADC subclasses the same base under the same class name with its own map (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs).
IntegrationEventPayloadPurityRuleTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:28· Level 13 · class
- What it is - the direct-fixture test of the two rules
IntegrationEventPayloadPurityTests(below) exercises indirectly: it callsArchitectureRules.IntegrationEventsLiveInSharedAssembliesandArchitectureRules.IntegrationEventPayloadsAreDomainFreeagainst two small purpose-built maps and a fixture event, and asserts on the exception message the rule throws. - Depends on - ArchitectureRules (
:33,:43,:51,:64,:74),ArchitectureMapBase(:78,:87), and two fixtures defined in the same file:FixtureCleanEvent,FixtureLeakingEvent/FixtureLeakedPayload, andFixtureLeakingEvent.Payload/.Entity. - Concept introduced - proving a rule fires by reading its failure message, not just its pass/fail. Four of the five
[Fact]s assert on.Which.Message, not merelyShould().Throw(), because an architecture rule that throws for the wrong reason is a false positive that still turns CI red.PayloadRule_FlagsADomainTypeReachedThroughANestedPayload(:49-:59) checks the message names the full PATH (FixtureLeakingEvent.Payload.Entity), not just the event, because a leak one level down is exactly the one a reviewer skims past in a shorter message. Two private nested maps carry the scope split:FrameworkProbeMap(:78-:84) has no module boundary, so the residency rule (which only fires inside a*.Sharedmodule assembly) is vacuous over it and only the payload rule is exercised;ModuleProbeMap(:87-:93) wraps the same assembly in aModule(...)layer so the residency rule has something to react to. [Rubric §7 - Microservices Readiness] is the property under test (a leaked domain type in an integration event payload is a hidden coupling between services); [Rubric §34 - Architecture Governance & Documentation] is the message-quality discipline. - Walkthrough - five
[Fact]s:ResidencyRule_FlagsAnEventOutsideASharedAssembly(:31-:39) runs the residency rule overModuleProbeMapand asserts the message namesFixtureCleanEventand says "is not a *.Shared assembly".ResidencyRule_IsVacuousForAModuleLessMap(:42-:46) runs it overFrameworkProbeMapand asserts no throw.PayloadRule_FlagsADomainTypeReachedThroughANestedPayload(:49-:59) andPayloadRule_LeavesACleanEventAlone(:62-:70) run the payload rule overFrameworkProbeMapagainst the leaking and the clean fixture respectively.PayloadRule_PassesOnTheFrameworksOwnAssemblies(:73-:75) runs the payload rule overCommonArchitectureMapitself and asserts it does not throw, namingOutputCacheEvictionRequestedin the assertion message as the concrete proof (a list of strings and nothing else). - Why it's built this way - splitting the residency and payload checks into their own maps keeps each
[Fact]a single-variable test: a residency failure and a payload failure are different rules with different scopes, and conflating them in one fixture would make a red run ambiguous about which rule broke. See ADR-015. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob; it is the fixture-level proof behind the subject-level assertion IntegrationEventPayloadPurityTests makes against the framework's own assemblies.
IntegrationEventPayloadPurityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs:18· Level 13 · class
- What it is - the MMCA.Common binding of the integration-event purity rule: a two-line subclass that supplies the repo's map so every integration event the framework defines is checked for a domain-free payload and (where applicable) residency in a
*.Sharedassembly. - Depends on -
IntegrationEventPayloadPurityTestsBase(public sealed class IntegrationEventPayloadPurityTests : IntegrationEventPayloadPurityTestsBase,:18) and CommonArchitectureMap (:20, the single override). - Concept introduced - cross-references IntegrationEventPayloadPurityRuleTests above for the rule mechanics; this subclass supplies no new behavior, only the map. [Rubric §7 - Microservices Readiness] applies the same way: a leaked domain entity in an event payload couples every consumer of that event to the producer's internals.
- Walkthrough - one member,
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();(:20). The base class contributes the[Fact]s that walk every integration event the map's layers expose. - Why it's built this way - the same zero-argument subclass shape used throughout this chapter for binding a shared rule base to a repo-specific map, so the rule runs once per repo with that repo's own event set.
- Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob. Subclassed the same way inMMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs.
Probe
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataResidencyTestsBaseTests.cs:24· Level 13 · class
- What it is - a private nested subclass of DataResidencyTestsBase inside DataResidencyTestsBaseTests, whose only job is to expose the base's protected
ContainsRegionClaimhelper to the test class (DataResidencyTestsBaseTests.cs:24-:31). - Depends on - DataResidencyTestsBase (
private sealed class Probe : DataResidencyTestsBase,:24), CommonArchitectureMap and the IArchitectureMap abstraction (:26). - Concept - a test-only subclass used as a key to a protected member.
ContainsRegionClaimisprotected staticon the base (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:57), so only a derived type can call it;Probederives, forwards it aspublic static bool Contains(string policy, string claim)(:28), and satisfies the two abstract members with the minimum:Map(:26) and anExtractDeployedRegionthat returnsstring.Empty(:30). It isprivatedeliberately: the base carries a[Fact]of its own, and a public subclass would be collected by xUnit and run that fact against the real repo root, which the framework repo has no deployed region for (class doc,DataResidencyTestsBaseTests.cs:6-:8). - Where it's used - only by
ContainsRegionClaim_MatchesWholeRegionTokensOnlyin the enclosing class (:20).
QueryHandlerReadRepositoryTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs.Repositories·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs:14· Level 13 · class
- What it is - the fixture-level proof of the query-handler-uses-read-repository convention rule: four
[Fact]s that runArchitectureRules.QueryHandlersUseReadRepositoriesagainst a small fixture assembly and assert on which handler names its failure message names. - Depends on - ArchitectureRules (
:141, called asArchitectureRules.QueryHandlersUseReadRepositories(_fixtureMap, allowed)),QueryHandlerReadRepositoryTestsBase(public sealed class QueryHandlerReadRepositoryTests : QueryHandlerReadRepositoryTestsBase,:14),ArchitectureMapBase(the nestedFixtureAssemblyMap,:151-:159), and three fixtures fromMMCA.Common.Architecture.Tests.ReadRepositoryFixtures:WriteRepositoryQueryHandlerFixture,ReadRepositoryQueryHandlerFixture,WriteRepositoryCommandHandlerFixture(:118,:123,:128). - Concept introduced - finding a repository call after an
await, inside the compiler's generated state machine. The first[Fact]'s own assertion message states the point directly: "the GetRepository call sits after an await, inside the compiler's state machine, and must still be found" (:116-:119). A query handler that callsGetRepositoryfor the write side only after awaiting some other call compiles to a state machine type, not the method body as written, so a naive IL scan over the declared method would miss it; this fixture exists specifically to prove the rule still catches that shape. [Rubric §6 - CQRS & Event-Driven] is the property: a query handler that reaches for the write repository is either a misclassified command or a read that will bypass whatever read-side optimization (a cached, projected, or replica-backed repository) the write repository does not offer. - Walkthrough - four
[Fact]s plus a privateReporthelper and a nestedFixtureAssemblyMap.QueryHandlerAskingForTheWriteRepository_IsFlagged_EvenInsideAnAsyncBody(:116-:119) asserts the report containsWriteRepositoryQueryHandlerFixture.QueryHandlerAskingForTheReadRepository_IsNotFlagged(:122-:123) asserts the opposite for the clean fixture.CommandHandlerAskingForTheWriteRepository_IsOutOfScope(:126-:129) asserts a command handler naming the write repository is never flagged, "a command handler writes, so the write repository is the right one there" (:129).AllowlistedHandler_IsSilenced(:132-:134) passes the write-repository query handler's full name into the rule'sallowedcollection and asserts it drops out of the report.Report(:137-:148) calls the rule inside atry/catch (XunitException)and returns either the empty string or the caught message, so each fact can assert on message content rather than only on throw/no-throw.FixtureAssemblyMap(:151-:159) is anArchitectureMapBasewhose only registered layer isFramework(Layer.Application, typeof(ReadRepositoryQueryHandlerFixture).Assembly), so the rule scans only this test assembly's fixtures rather than the framework's real Application layer. - Why it's built this way - proving the rule by message content, the same idiom IntegrationEventPayloadPurityRuleTests uses, catches a rule that throws for the wrong reason; and routing the scan through a fixture-only map keeps the real
CommonArchitectureMapfree of deliberately-wrong handlers that would otherwise have to be allowlisted permanently just to keep this test passing. See ADR-015. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob; its fixtures live inReadRepositoryFixtures.csin the same assembly, and MMCA.ADC subclasses the same base against its own map (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs).
RawSqlConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Cqrs·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawSqlConventionTests.cs:12· Level 13 · class
- What it is - the MMCA.Common binding of the raw-SQL convention rule: a subclass that scans the framework's own Application and Infrastructure source for interpolated or concatenated SQL text and allowlists the one file that legitimately needs it.
- Depends on -
RawSqlConventionTestsBase(public sealed class RawSqlConventionTests : RawSqlConventionTestsBase,:12) and CommonArchitectureMap (:14);ArchitectureMapBase.FindRepoRoot(:19) locates the solution root the scan walks from. - Concept introduced - scanning source text, not the compiled model, because the risk (SQL injection) lives in the string literal, not in a type reference a Mono.Cecil rule can see.
ScannedSourceDirectories(:17-:22) points the base's text scan atSource/Core/MMCA.Common.ApplicationandSource/Core/MMCA.Common.Infrastructure, the two layers where a hand-built query could appear.AllowedFiles(:25-:32) allowlists exactly one file,DbContextFactory.cs, for aSET IDENTITY_INSERT [schema].[table] ON/OFFstatement whose table and schema names come off EF model metadata (entityType.GetSchema()/GetTableName()) rather than caller input, because T-SQL has no parameterized form for an identifier in that position. [Rubric §29 - Security Practices] is the property: the rule fails CI on any new interpolated SQL string outside this one reviewed exception, catching the injection surface before a query with caller-controlled text merges. [Rubric §34 - Architecture Governance & Documentation] is the allowlist-with-reasons discipline; the comment names the matchingS2077Sonar suppression at the call site itself, so the same exception is argued in two places that have to agree. - Walkthrough - two members.
ScannedSourceDirectories(:17-:22) yields the two directory paths under the discovered repo root.AllowedFiles(:25-:32) is a one-entryIReadOnlyList<string>with the justification comment (:27-:30) directly above the filename (:31). - Why it's built this way - text-scanning rather than model-scanning means the rule also catches raw SQL that never reaches EF Core at all (a
SqlCommandbuilt by hand, for instance), which a Cecil-based rule over compiled IL would only see after the fact if it used a parameter, not a literal. See ADR-015. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob. Subclassed the same way inMMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs.
ServiceContractPurityTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Layering·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11· Level 13 · class
- What it is - the MMCA.Common binding of the
[ServiceContract]purity rule: a two-line subclass that supplies the repo's map so any type marked as part of a published wire surface is checked for dependencies on the producing service's Domain, Application or Infrastructure. - Depends on - ServiceContractPurityTestsBase (
public sealed class ServiceContractPurityTests : ServiceContractPurityTestsBase,ServiceContractPurityTests.cs:11), IArchitectureMap and CommonArchitectureMap (the single override,:13), and indirectly ServiceContractAttribute, which the rule matches by full name rather than by reference. - Concept introduced - a rule that is deliberately vacuous today, kept as a ratchet. Most gates in this chapter earn their place by failing on real code. This one asserts nothing in MMCA.Common, because the framework marks no type with
[ServiceContract], and both the subclass doc (:9-:10) and the base's remarks (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:12-:17) say so plainly. The value is in the timing, the same argument the three ratchet subclasses rest on: the invariant is enforced from the first marked type onward, with no test for anyone to remember to write, at the moment when a contract package's shape is still cheap to change. Two design choices follow from that. It is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer today and a layer-iterating rule would pass vacuously forever (base remarks,:9-:11); and it scans every assembly the map registers, so a marked type is judged wherever it lives. A marked type sitting inside a Domain, Application or Infrastructure assembly then fails by construction, which the rule's own remarks call the intent: a published contract belongs in a*.Contractsor Shared assembly, not inside the service it describes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:26-:29). [Rubric §7 - Microservices Readiness] is the property: a contract that leaks a domain entity, a handler abstraction or a persistence type forces every consumer to take the producer's internals as a package dependency, which is what makes an extraction irreversible (:16-:18). [Rubric §9 - API & Contract Design] covers the wire surface itself, and [Rubric §34 - Architecture Governance & Documentation] the ratchet. - Walkthrough - one member,
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();(:13). Everything else is inherited: the base contributes a single[Fact],ServiceContracts_ShouldNotDependOn_ServiceInternals(ServiceContractPurityTestsBase.cs:24-:26), which callsArchitectureRules.ServiceContractsDoNotDependOnServiceInternals(Map). The rule first derives the forbidden namespace set from the map's Domain, Application and Infrastructure layers (ArchitectureRules.Contracts.cs:57-:66) and returns immediately when that set is empty (:35-:38), then walks every layer, selects marked types through the Mono.Cecil custom ruleCarriesServiceContractAttribute(:44,:69-:74), and assertsShouldNot().HaveDependencyOnAny(forbidden)per layer with the layer's root namespace in the message (:42-:52). - Why it's built this way - matching the marker by its full-name string,
"MMCA.Common.Shared.Abstractions.ServiceContractAttribute"(ArchitectureRules.Contracts.cs:10-:11), is the same zero-reference idiom the rest of the rule library uses: the testing package deliberately takes no compile dependency on the framework assemblies it inspects. The rule complements, and does not replace, the transport- and layer-purity rules that guard the same boundary from the layer side (ADR-007, ADR-015). - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob. Its siblings in this chapter are ProtoContractFitnessTests and ContractImplementationTests, the other consumer-facing contract gates the framework exercises without owning any subject of its own. - Caveats / not-in-source - because the framework marks no type, this class asserts nothing today; there is no fixture proving the rule fires. That proof exists only in whichever repo first marks a type.
StronglyTypedIdConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdConventionTests.cs:12· Level 13 · class
- What it is - the framework repo's binding of the strongly typed identifier fitness function (ADR-115): a three-line sealed subclass that supplies the map and inherits the one
[Fact], so any wrapper the framework repo declares is held to the readonly-record-struct shape. - Depends on - StronglyTypedIdTestsBase (
public sealed class StronglyTypedIdConventionTests : StronglyTypedIdTestsBase,StronglyTypedIdConventionTests.cs:12), CommonArchitectureMap and the IArchitectureMap abstraction it satisfies (StronglyTypedIdConventionTests.cs:14). - Concept - the same base-plus-thin-subclass shape as UIArchitectureConventionTests and CancellationTokenConventionTestsBase: the rule lives once in
MMCA.Common.Testing.Architectureand is re-run per repo by a subclass whose only job is to name the repo. The rule is deliberately vacuous today: every repo still defaults to the primitive identifier aliases (ADR-048/ADR-085), soMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.StronglyTypedIds.cs:38-:41finds zero matching types across the map's layers and the rule passes with nothing asserted. Its purpose is to hold the FIRST wrapper a repo declares to the shape the framework's EF converter, JSON converter, comparer and route binding are written against (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:4-:8). - Walkthrough - one member:
protected override IArchitectureMap Map { get; } = new CommonArchitectureMap();(StronglyTypedIdConventionTests.cs:14). The inherited fact,StronglyTypedIds_ShouldBe_ReadonlyRecordStructs, callsArchitectureRules.StronglyTypedIdsAreReadonlyRecordStructs(Map)(StronglyTypedIdTestsBase.cs:14-:15), which loads every type across the map's distinct layer assemblies, filters to types implementingIStronglyTypedIdby full-name prefix match, and asserts three properties on each match: it is a value type (ArchitectureRules.StronglyTypedIds.cs:43-:45), declaredreadonly(:47-:49), and a record (:51-:53), plus that it declares no state beyond the singleValueproperty. - Why it's built this way - the three checked properties are load-bearing rather than stylistic:
structkeeps an identifier allocation-free on the hot path a primitive alias occupies today,readonlyprevents a tracked entity's key from changing under it, andrecordsupplies the structural equality the wrapper exists for (ArchitectureRules.StronglyTypedIds.cs:14-:22). The rule is kept separate from the value-object immutability rule because an identifier has no validation to fail and noResult-returning factory (:26-:29). - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; nothing constructs it. No other repo subclassesStronglyTypedIdTestsBasetoday: the aliases remain the default everywhere, so this is currently the only binding.
TenantEntityConventionTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain.EntityModel·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/EntityModel/TenantEntityConventionTests.cs:20· Level 13 · class
- What it is - a framework-only tenancy marker rule (rubric section 4): two
[Fact]s asserting that every concrete MMCA.Common entity that declares or inherits aTenantIdproperty implements ITenantEntity (class doc,TenantEntityConventionTests.cs:6-:11). Unlike its neighbours in this part it is not a subclass of a shared*TestsBase; the rule exists only in this repo. - Depends on - CommonArchitectureMap (constructed directly,
:47), the Layer enum (:49), ITenantEntity (:35), and, by full-name prefix only, BaseEntity<TIdentifierType> (BaseEntityFullNamePrefix,:22). - Concept introduced - a column that looks scoped but is not.
ITenantEntityis the marker that applies the namedTenantquery filter and theTenantSaveChangesInterceptorstamp, so an entity carrying a tenant column without it is readable and writable across tenants while appearing tenant-scoped (:9-:11). The rule catches that by structure (a property namedTenantId, public or not,:23,:34) rather than by trusting the author to remember the marker. Equally deliberate is what it leaves out: the population is theBaseEntity<TId>hierarchy only, so the outbox, internal-command and audit-trail rows fall outside it. Those rows RECORD the tenant of the scope that wrote them (a nullable ambient-context column restored on delivery) rather than being owned by a tenant, and a read filter on them would hide work from the background drainers (:12-:18). [Rubric §4 - DDD] is the category the doc cites; multi-tenant isolation is the property it protects. - Walkthrough -
FrameworkEntities(:45-:54) unions the map's Domain layer with its Infrastructure assemblies (:49-:51), de-duplicates, and keeps concrete classes for whichDerivesFromBaseEntityis true (:53).DerivesFromBaseEntity(:56-:68) walks the base-type chain, reducing each generic base to its definition, and matches the full name againstMMCA.Common.Domain.Entities.BaseEntity`by ordinal prefix (:58-:64), so every arity ofBaseEntityand every subclass of it (auditable, aggregate root) qualifies.FrameworkEntities_AreDiscovered_GateIsNotVacuous(:25-:28) asserts the population is non-empty;EntitiesWithTenantId_ShouldImplement_ITenantEntity(:30-:42) selects entities that expose aTenantIdinstance property but are not assignable toITenantEntityand fails with their full names (:33-:41). - Why it's built this way - the non-vacuous guard sits beside the real assertion because both halves of the population query are name- and layer-driven: a renamed
BaseEntityor a map that stopped registering Domain would otherwise leave a rule that passes over an empty set. - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Testsin CI'sbuild-and-testjob; nothing constructs it and no consumer repo carries a counterpart.
UpcasterTestMap
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Contracts·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventUpcasterFitnessTests.cs:85· Level 13 · class
- What it is - a one-layer architecture map used only by EventUpcasterFitnessTests: it registers this test assembly as the map's single Application layer so the two upcaster rules see the fixture upcasters and nothing else.
- Depends on - ArchitectureMapBase (
private sealed class UpcasterTestMap : ArchitectureMapBase,EventUpcasterFitnessTests.cs:85), LayerRef, and the Layer enum. - Concept - cross-references the map concept from CommonArchitectureMap, with one detail specific to these rules. Both of them scope by ownership exactly as the integration-event rules do:
EventUpcastersincludes framework layers only when the map declares no modules (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Upcasters.cs:81-:64). Because this map is built entirely fromFramework(...)entries,ModuleNamesis empty and the single Application layer is scanned, which is what puts the fixtures in scope. Registering a module layer instead would silently exclude them and both rules would pass vacuously, so the choice is load-bearing rather than incidental. Compare FakeConsumerMap and FixtureModuleMap, which flip exactly that switch on purpose for rules that scope the other way. - Walkthrough -
RepoToken => "MMCA.Common"(:76) and a one-entryDefineLayers()returningFramework(Layer.Application, typeof(EventUpcasterFitnessTests).Assembly)(:78-:79). The doc comment states the intent in one line (:73). The layer's derived root namespace is unused by these rules, which enumerateConcreteClassesacross whole assemblies (ArchitectureRules.Upcasters.cs:86) rather than namespace-scoped subsets. - Where it's used - constructed on every call of the test class's two private rule helpers (
EventUpcasterFitnessTests.cs:72,:68).
SoftDeleteEnforcementTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Domain·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19· Level 13 · class
- What it is - the MMCA.Common binding of the hard-delete rule, and the one thin subclass in this unit whose override is a substantial, individually-argued document: the six framework types that are allowed to erase rows, each with the reason written next to it.
- Depends on - SoftDeleteEnforcementTestsBase (
public sealed class SoftDeleteEnforcementTests : SoftDeleteEnforcementTestsBase,SoftDeleteEnforcementTests.cs:19) and CommonArchitectureMap (:21). - Concept introduced - the rule turned back on its own author, and why that keeps a downstream allowlist honest. Store and ADC subclass the same base and allowlist exactly these six framework types, because no module of theirs erases a row of its own. Running the rule here is what keeps that list correct: a NEW framework eraser would otherwise land in the packages and fail downstream, in a repo whose maintainers did not write it and cannot judge it, rather than in the repo where the change was made (class doc,
:10-:17). The second decision in that doc is finer: each entry is named individually rather than exempting theMMCA.Common.Infrastructure.Persistencenamespace, so a seventh eraser under that namespace still fails here and gets reviewed. A namespace exemption would have been two characters shorter and would have silently pre-approved every future eraser in the framework's persistence layer. [Rubric §8 - Data Architecture] and [Rubric §30 - Compliance/Privacy/Data Governance] are the properties (ADR-005); [Rubric §34 - Architecture Governance & Documentation] is the allowlist-with-reasons discipline; [Rubric §33 - Developer Experience] is the failure-location argument. - Walkthrough - two members.
Map(:21) is the usual override.AllowedHardDeleteTypes(:24-:60) is the substance, six fully-qualified entries each preceded by its justification:- EFRepository<TEntity, TIdentifierType> (
:31, listed by its arity-suffixed metadata name underMMCA.Common.Infrastructure.Persistence.Repositories), the framework's set-based delete escape hatch. The comment states the calling convention that makes it acceptable (:26-:30): erasing is the caller's explicit ask (the method is namedExecuteDeleteAsync), and it targets derived rows the caller is about to rewrite, never user data carrying an audit or erasure obligation. Allowlisting the implementation rather than the abstraction is what leavesIRepositoryfree to be used everywhere else. - OutboxCleanupService (
:36, now under...Persistence.Outbox.Administration), forPurgeAsync,PurgeInboxAsyncandSweepDeadLettersAsync. These rows are delivery plumbing with a bounded lifetime, and the sweep is the retention policy; soft-deleting them would grow the table the job exists to bound (:33-:35). - InternalCommandCleanupService and InternalCommandAdministration (
:42-:43), the outbox precedent applied to deferred work: an internal-command queue row is execution plumbing with a bounded lifetime whose payload can carry personal data, so the sweep IS the retention policy, and the operator-facing purge beside it erases only rows that already completed (:38-:41). - AuditTrailCleanupJob (
:47), for audit-trail retention. The comment inverts the usual instinct (:45-:46): erasing past the retention window IS the requirement, and keeping an audit row forever is the privacy defect rather than the safeguard. - RefreshSessionCleanupService (
:53), for refresh-session retention. A session row is framework bookkeeping rather than an aggregate: it carries noIsDeletedflag and no audit stamps, and its content is a credential digest plus the IP and user-agent of a device, so flagging it instead of erasing it would keep a growing record of a data subject's devices past any use for it (:49-:52). - EFPermissionGrantStore (
:59), for stored permission grants. A grant row is a set membership, not an aggregate: revoking a capability means the row is not there, and a soft-deleted grant would have to be filtered out of every authorization read forever, with a missed filter failing OPEN. It carries no audit or erasure obligation either, since its whole content is a role name and a permission name (:55-:58).
- EFRepository<TEntity, TIdentifierType> (
- Why it's built this way - because the entries are fully-qualified type names, a namespace move is a test failure until the list is updated: the 2026-09 folder reorganization that put the outbox cleanup job under
Outbox/Administrationhad to be reflected here (:36). The internal-command and permission-grant entries were added on top of the original four once those stores shipped, following the exact same precedent (the outbox entry) rather than a new argument. Six reasons written in a compiled file are also six reasons that get read in a diff when someone adds a seventh. That is the whole argument for an allowlist over a suppression: the list is where the erasure policy actually lives. See ADR-005 and ADR-015; the rule's own behavior is proven against compiled fixtures by SoftDeleteEnforcementFitnessTests. - Where it's used - run by the
MMCA.Common.Architecture.Testssuite in CI'sbuild-and-testjob. The same entries appear in the Store and ADC subclasses of the same base (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs).
DataResidencyTestsBaseTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Governance·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DataResidencyTestsBaseTests.cs:10· Level 14 · class
- What it is - the unit test for the region comparison inside the data-residency gate: one six-case
[Theory]that pinsDataResidencyTestsBase.ContainsRegionClaimto whole-token matching (DataResidencyTestsBaseTests.cs:12-:21). - Depends on - DataResidencyTestsBase, reached through its nested Probe subclass (
:24-:31). - Concept introduced - testing the helper because the gate itself reads the real repo. The base's one fact,
PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion, locates the repo root, parses the deployed region from a repo-specific source and readsPRIVACY.md(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:26-:45), so it cannot be fed fixtures; the comparison helper is the testable unit (class doc,DataResidencyTestsBaseTests.cs:6-:8). The failure it guards is a false pass: Azure region names nest (westusis a prefix ofwestus2,centralusis a suffix ofsouthcentralus), so a naive substring check would let a policy claiming "West US 2" satisfy a deployment inwestus. The helper normalizes both sides (whitespace stripped, upper-cased) and accepts an occurrence only when no digit follows it and the text before it does not end in a directional prefix (DataResidencyTestsBase.cs:47-:79). [Rubric §30 - Compliance/Privacy/Data Governance] is the property (ADR-105). - Walkthrough -
ContainsRegionClaim_MatchesWholeRegionTokensOnly(policy, claim, expected)(:19-:22) callsProbe.Containsand asserts the expected boolean, with the because-text "a region that is a prefix of another region must not satisfy the residency gate" (:22). The six[InlineData]rows (:13-:18) pair the negatives with their positives:westusagainst "West US 2" is false whilewestus2is true;centralusagainst "South Central US" is false while against "Central US" it is true; and two rows prove markdown emphasis (**West US 2**) and a spelled-out claim ("Central US") still match. - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests; the gate it backs runs in consumer repos as thin subclasses such as DataResidencyTests in MMCA.ADC.
FormsConventionRequirednessFitnessTests
MMCA.Common.Architecture.Tests ·
MMCA.Common.Architecture.Tests.Ui·MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:12· Level 14 · class (public, sealed)
- What it is - the fixture-level proof of the create-form requiredness fact in FormsConventionTestsBase: two
[Fact]s that runAdminCreateForms_ReadRequirednessOffTheirModelagainst two small.razorfixture trees and assert it fails on one and passes on the other (FormsConventionRequirednessFitnessTests.cs:22-:36). - Depends on - FormsConventionTestsBase (the private nested
FixtureFormsTests,:38), CommonArchitectureMap and the IArchitectureMap abstraction (:40),ArchitectureMapBase.FindRepoRoot(:15, see ArchitectureMapBase), andXunitExceptionfromXunit.Sdk(:2,:27). Its subjects are four fixture files underRazorFixtures/Forms(:14-:20):Commented/WidgetCreate.razorandCommented/WidgetFormFields.razor,Clean/GadgetCreate.razorandClean/GadgetFormFields.razor. - Concept introduced - a marker that is present in the text but absent from the page. The requiredness fact is a text scan: it passes a create form when its own markup, or any sibling
*FormFields.razorbeside it, containsModelValidation.IsRequired(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:180-:203). A text scan alone would accept a marker that sits inside a comment and renders nothing. The base therefore strips Razor (@* *@) and HTML (<!-- -->) comments from each file before it joins the scan (:187,:192, the stripper at:263-:264), and this class is the regression proof of that step (class doc,FormsConventionRequirednessFitnessTests.cs:6-:11, which tags the backlog item M258). TheCommentedfixture hides the marker in both possible places: a Razor comment in the page (RazorFixtures/Forms/Commented/WidgetCreate.razor:1-:4) and an HTML comment in the sibling field component (RazorFixtures/Forms/Commented/WidgetFormFields.razor:1). TheCleanfixture is the positive control for the extraction the fact follows: the page declares nothing itself (RazorFixtures/Forms/Clean/GadgetCreate.razor:1-:5) and its sibling carries the live marker (RazorFixtures/Forms/Clean/GadgetFormFields.razor:1). [Rubric §24 - UX Safety] is the property the base guards (its failure message names the §24 model-sourced validation bridge,FormsConventionTestsBase.cs:202); [Rubric §34 - Architecture Governance & Documentation] is proving a gate can fail, not only that it passes. - Walkthrough
FormsFixtureRoot(FormsConventionRequirednessFitnessTests.cs:14-:20): theRazorFixtures/Formsfolder resolved from the repo root located byMMCA.Common.slnx.ACommentedOutRequirednessMarker_DoesNotSatisfyTheGate(:22-:28): points the fixture subclass atCommented, invokes the fact directly, and asserts it throws anXunitExceptionwhose message containsWidgetCreate.razor(:27), so the failure is pinned to the right form rather than to any throw.ALiveMarkerInTheSiblingFieldComponent_SatisfiesTheGate(:30-:36): points it atCleanand asserts no throw.FixtureFormsTests(string modulesRoot)(:38-:43): a private sealed primary-constructor subclass that overridesMapwithnew CommonArchitectureMap()(:40) andModulesRootwith the fixture folder (:42).ModulesRootis the base's virtual scan root,Source/Modulesunder the repo root by default (FormsConventionTestsBase.cs:55-:60); the fixture folders each hold one*Create.razor, which meets the base's defaultMinimumCreateFormsof 1 (:53) so the discovery guard inDiscoverCreateForms(:233-:248) does not fire first.
- Why it's built this way - the subclass is private so xUnit does not discover it and run the base's facts over the fixture trees as real tests; the one fact under test is invoked by hand instead (class doc,
FormsConventionRequirednessFitnessTests.cs:9-:10). Real.razorfiles rather than inline strings keep the fixtures on the same file-reading path the gate uses in production, the same choice SortableColumnFitnessTests and InlineStyleFitnessTests make for their text-scanning rules. - Where it's used - discovered and run by xUnit as part of
MMCA.Common.Architecture.Tests(no usage sites outside the defining file). The base it proves runs for real in consumer repos as thin subclasses such as FormsConventionTests in MMCA.ADC.
CrossServiceDataSource
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15· Level 0 · sealed record
- What it is: a two-field record naming one logical data source a cross-service fixture routes to its
own physical database: the logical name the framework's
DataSourcesconfiguration section keys on (normally the module name) and the database that name resolves to on the shared SQL Server container (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:8-15). - Depends on: nothing. A positional
sealed recordof two strings, declared above CrossServiceFixtureBase in the same file. - Concept: it is the declarative half of database-per-service
(ADR-006, taught in
primer §2) expressed as test data. The
doc's own examples are the shape to hold onto:
LogicalNameisConference,DatabaseNameisADC_Conference(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:13-14).[Rubric §8, Data Architecture]assesses whether each service owns its own store; this record is how a test fixture states that ownership once and derives everything else from it. - Walkthrough: two positional members,
LogicalNameandDatabaseName(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:15), so it gets structural equality and immutability for free. The base consumes each instance three ways: the database name drives the pre-create loop (CrossServiceFixtureBase.cs:219), and the logical name drives both environment keysSetNamedDataSourcepushes,DataSources__{LogicalName}__SQLServerConnectionStringandDataSources__{LogicalName}__SQLServerMigrationsAssembly(CrossServiceFixtureBase.cs:278-281). - Where it's used: as the
DataSourceslist a subclass supplies (CrossServiceFixtureBase.cs:66); ADC declares three, Identity, Conference and Engagement (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:62-67), and Store declares two, Catalog and Sales (MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:56-60).
DependencyInjectionAssert
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:13· Level 0 · class (static)
- What it is: a one-method assertion helper for the DI registration extensions every module and layer
exposes. It proves a registration extension hands back the very
IServiceCollectionit was given, so a fluent chain stays intact. - Depends on:
AwesomeAssertionsandMicrosoft.Extensions.DependencyInjection(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:1-2). No first-party dependency. - Concept introduced, the fluent-contract guard. The framework's registration methods are fluent by
convention: hosts chain
AddApplication().AddInfrastructure(...).AddAPI(...). An extension that returns a new collection silently drops every registration chained after it, and no other test catches that, because the dropped services are simply absent rather than wrong (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:6-11).[Rubric §14, Testability]assesses whether an invariant can be checked cheaply; this one turns an otherwise invisible composition failure into a one-line test.[Rubric §15, Best Practices & Code Quality]covers the convention itself: the return-the-same-collection contract is what lets host composition stay declarative. - Walkthrough
ReturnsSameCollection(Func<IServiceCollection, IServiceCollection> register)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/DependencyInjectionAssert.cs:21-32): null-guards the delegate (:23), creates theServiceCollectionitself so the call site stays one line (:25, the doc shows the shape at:16-18), invokes the registration under test (:27), and assertsresult.Should().BeSameAs(services, ...)with a because-reason that spells out the consequence of failing (:29-31).- Reference equality is the whole assertion. It deliberately says nothing about what was registered; the per-module tests that call it assert their own service descriptors separately.
- Why it's built this way: creating the collection inside the helper is what keeps adoption free. A module's DI test adds one line per registration extension rather than three lines of arrange plus an assertion nobody remembers to write.
- Where it's used: seven module DI test classes across the two apps, for example
MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Infrastructure.Tests/DependencyInjectionTests.cs:29,MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/DependencyInjectionTests.cs:63,68,MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/DependencyInjectionTests.cs:68,73,MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/DependencyInjectionTests.cs:49,54,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/DependencyInjectionTests.cs:26,31, andMMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/DependencyInjectionTests.cs:35. MMCA.Common self-tests the helper, including that it fails for an extension returning a different collection (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/DependencyInjectionAssertTests.cs:13).
Direction
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:67· Level 0 · enum (private, nested)
- What it is: a two-value private enum nested in
OpenApiBreakingChangeDetector, naming which way a schema travels:RequestorResponse(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:67-71). - Depends on: nothing.
- Concept introduced, direction-sensitive compatibility. The same schema edit is breaking in one direction
and harmless in the other.
[Rubric §14, Testability]is served by making that asymmetry an explicit input rather than two copies of the comparison. - Walkthrough
Responseis passed when walking response bodies (:394); a baseline property missing from the live schema is then reported asRemovedResponseProperty(:477-480).Requestis passed for request bodies and parameter schemas (:323,:378); on this side the check is instead about newly required properties (:484).
- Why it's built this way: a client sends requests and reads responses, so removing a response property
breaks readers, while adding a required request property breaks writers. One recursive
CompareSchemathreads the enum instead of duplicating the walk. - Where it's used: only inside the defining file; nothing outside it references the type.
EntityBuilderBase<TBuilder, TEntity>
MMCA.Common.Testing ·
MMCA.Common.Testing.Builders·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9· Level 0 · class (abstract)
- What it is: the tiny root of the framework's fluent test-data builders. A subclass fixes sensible
defaults for one entity type so a test only has to state the properties it actually cares about, then
calls
Build()to materialize the entity through its real domain factory. - Depends on: nothing first-party, and no BCL surface beyond
object. Two type parameters and one abstract method is the whole type (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-18). - Concept introduced, the Test Data Builder plus the self-referencing generic (CRTP).
[Rubric §14, Testability]assesses how easily the code can be exercised in isolation; a builder base is a textbook §14 affordance, it removes the copy-pasted setup that otherwise bloats every arrange step. The signatureEntityBuilderBase<TBuilder, TEntity> where TBuilder : EntityBuilderBase<TBuilder, TEntity>(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:9-10) is the curiously-recurring template pattern: a concrete builder passes itself asTBuilder, so theWithX(...)methods a subclass adds can return the concrete builder type and keep a fluent chain strongly typed without a cast. - Walkthrough
Build()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Builders/EntityBuilderBase.cs:17): the single abstract member. The XML doc (EntityBuilderBase.cs:12-15) records the contract, the subclass calls the entity's Result-returning factory (ADR-013) and throws if it failed, so a builder never yields a domain object that violated its invariants. The base deliberately owns no state and no defaultWithXhelpers, those live on each concrete builder because defaults are per-entity.
- Why it's built this way: keeping the base to one abstract method means it adds zero coupling and
zero opinions beyond "a builder produces a
TEntity". The CRTP is the only structural rule it enforces, and it exists purely so fluent chaining stays type-safe down in the subclasses. - Where it's used: the domain-test builders in both apps subclass it, ten today:
MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Builders/ActivityBuilder.cs:10,.../Builders/EventBuilder.cs:10,.../Builders/SessionBuilder.cs:10,.../Builders/SpeakerBuilder.cs:10,.../Builders/SponsorBuilder.cs:11,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Domain.Tests/Builders/UserBuilder.cs:10,MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.Domain.Tests/Builders/CategoryBuilder.cs:10,.../Builders/ProductBuilder.cs:10,MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.Domain.Tests/Builders/CustomerBuilder.cs:11, andMMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.Domain.Tests/Builders/OrderBuilder.cs:11. - Caveats / not-in-source: the "throws on failure" and "sensible defaults" behavior is documented on the base but implemented only in those subclasses, which live outside this unit.
FeatureManagementTestExtensions
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:10· Level 0 · class (static)
- What it is: a one-method helper that lets an integration-test host force feature-flag values,
overriding whatever
appsettings.jsonwould otherwise resolve, so a test can pin a flag on or off and assert both branches of a feature-gated command or query. - Depends on: BCL and NuGet only,
IServiceCollectionandIConfigurationfromMicrosoft.Extensions.*plusAddFeatureManagementfromMicrosoft.FeatureManagement(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:1-3). No first-party dependency. - Concept: this is the test-side counterpart to the framework's
FeatureGateCommandDecorator<TCommand, TResult>,
the outermost link in the CQRS pipeline (taught in
primer §2).
[Rubric §14, Testability]again: a gated handler is only meaningfully testable if a test can flip its flag deterministically.[Rubric §17, DevOps & Deployment]applies too, feature management (ADR-031) is a cross-cutting concern, and this helper keeps its test-time configuration in one reusable place. - Walkthrough
- The whole class body is a single C# preview
extension(IServiceCollection services)block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:12), the same extension-member style the framework uses for DI registration (see primer §4), not a classicthis-parameter extension method. ConfigureTestFeatureFlags(Dictionary<string, bool> features)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35-62): null-guards the dictionary (:38), then layers the flags on top of the configuration the host already registered rather than replacing it. It finds the lastIConfigurationdescriptor and reads itsImplementationInstance(:40-42), seeds aConfigurationBuilderfrom it when one was found (:44-48), appends the flags as in-memoryFeatureManagement:{Name}keys so they win over anyFeatureManagementsection the host configured (:50-56), registers the composed root as theIConfigurationsingleton (:58), callsAddFeatureManagementagainst itsFeatureManagementsection (:59), and returns the collection for chaining (:61).- The layering is the load-bearing detail, and the doc says why (
:18-25): .NET DI hands a non-collection dependency the last registration, so building a flags-only root here would give every component constructed afterwards a configuration containing nothing butFeatureManagement, silently losing connection strings, authentication settings and the data-source section. - The pick-up is instance-only by design (
:26-31): a configuration registered behind a factory cannot be read without building the provider, so in that case the flags stand alone.
- The whole class body is a single C# preview
- Why it's built this way: pushing overrides through the real
IConfigurationplusAddFeatureManagementpath (rather than mocking anIFeatureManager) means the test exercises the same feature-evaluation code the production host runs, only the source of the flag value changes. - Where it's used: it is intended for a test
WebApplicationFactory'sConfigureServices, and the XML doc says exactly that (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:14-17). MMCA.Common covers all three behaviors directly: that the flags layer on top of a host configuration (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/FeatureManagementTestExtensionsTests.cs:20), that they drive a resolvedIFeatureManager(:43), and that a collection with no configuration at all still gets the flags (:62). - Caveats / not-in-source: as of this pass no application test project calls it. A workspace-wide
search finds
ConfigureTestFeatureFlagsonly at its definition (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/FeatureManagementTestExtensions.cs:35) and in MMCA.Common's own unit tests; neither ADC, Store nor Helpdesk uses it. It ships in the package as available capability, not as a technique any application suite currently uses.
IIntegrationTestFixture
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:8· Level 0 · interface
- What it is: the contract every integration-test fixture implements, the two capabilities a test
base needs from a booted host: hand me an
HttpClient, and reset the database to clean between tests. - Depends on: BCL only (
HttpClient,Task). No first-party dependency, which is what lets it sit at Level 0 and be referenced by everything above it. - Concept introduced, the test fixture as an abstraction boundary.
[Rubric §14, Testability]: by depending on this interface rather than a concreteWebApplicationFactory, the reusable IntegrationTestBase<TFixture> stays host-agnostic, and each downstream app supplies its own concrete fixture with its ownProgram, JWT keys, and data sources. This is the boundary that keeps the shared test scaffolding inMMCA.Common.Testingand the app-specific wiring in each repo, which is the whole premise of ADR-058. - Walkthrough
CreateClient()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:11): returns anHttpClientconfigured for the in-process test server.ResetDatabaseAsync()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IIntegrationTestFixture.cs:19): resets the database between tests (the doc names Respawn as the typical mechanism). The doc comment (IIntegrationTestFixture.cs:13-18) records a load-bearing rule for the database-per-service topology (ADR-006): a host with multiple physical data sources must reset every relational source, and can enumerate them by resolving IEntityDataSourceRegistry and IDataSourceResolver from the host's services.
- Why it's built this way: two members, no state, no host coupling. The interface is deliberately minimal so the reset strategy (single database versus multi-source) is the fixture's problem, not the base's.
- Where it's used: implemented by
SqlServerIntegrationTestFixtureBase<TEntryPoint>
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27) and through it by every per-service fixture in both apps; consumed as theTFixtureconstraint on IntegrationTestBase<TFixture> (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14) and therefore by all three contract bases in this unit.
JwtTokenGenerator
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:30· Level 0 · class (static)
- What it is: a static factory that mints signed JWT bearer tokens for integration tests, plus the
matching switch that re-points a test host's Bearer scheme at the same committed key. Together they let a
test call an authorized endpoint as any role or user without standing up the real login flow or a
reachable JWKS endpoint. Each downstream project wraps the generator with role-specific convenience
methods (AdminToken, OrganizerToken, and so on,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:11-12). - Depends on: BCL and NuGet only,
System.Globalization,System.IdentityModel.Tokens.Jwt,System.Security.Claims,System.Security.Cryptography(RSA),Microsoft.AspNetCore.Authentication.JwtBearer(for the options type the second member configures), andMicrosoft.IdentityModel.Tokens(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:1-6). The generated claim layout mirrors the framework's ITokenService so downstream auth middleware cannot tell a test token from a real one (JwtTokenGenerator.cs:99-102). TheuserIdparameter is typedUserIdentifierType(JwtTokenGenerator.cs:114), the solution-wide identifier alias (ADR-048). - Concept introduced, exercising the real RS256 path in tests.
[Rubric §11, Security]assesses how authentication and key handling are done; the deliberate choice here is that tests sign with RS256 (SecurityAlgorithms.RsaSha256,JwtTokenGenerator.cs:131) using an embedded RSA-2048 dev keypair, the same asymmetric algorithm production uses, so integration tests run the identical validation code path (ADR-004, taught in primer §2) rather than a weaker HMAC shortcut.[Rubric §14, Testability]covers the ergonomics: deterministic tokens with no per-run key generation, and a host that validates them with no network dependency at all. - Walkthrough
- Public constants (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:33-96):DefaultIssuer(https://localhost:6001, line 33),DefaultKeyId(mmca-test-key, line 41, thekidthe host advertises on its JWKS document), and the pairedDefaultPublicKeyPem(line 49) andDefaultPrivateKeyPem(line 68). The class doc records the wiring contract: test host appsettings setJwt:SigningAlgorithm=RS256,Jwt:RsaPublicKeyPem, andJwks:KeyId(JwtTokenGenerator.cs:18-20) so RsaJwksProvider publishes a JWKS entry with the matchingkid. GenerateToken(...)(JwtTokenGenerator.cs:112-156): imports the PEM private key intoRSAParametersinside ausingso theRSAinstance can be disposed without invalidating the key held bySigningCredentials(:121-131), assembles the claim set (:137-141) and appends any caller-supplied extras (:143-146), then writes a one-hour token (:148-155). Defaulted parameters mean a caller normally passes only audience, user id, and role (:112-119).- The claim set is exactly two claims,
ClaimTypes.NameIdentifiercarrying the culture-invariant user id andClaimTypes.Role(:137-141).NameIdentifieris what the JWT bearer handler produces for a real token'ssubunder its default inbound mapping, so a test token carrying it reaches every reader the way a production one does; the comment above it (:133-136) records that the duplicate custom claim that used to ride alongside it is gone, matchingITokenService, because two claims for one identity can disagree. ConfigureInProcessTokenValidation(JwtBearerOptions options, string audience)(JwtTokenGenerator.cs:170-192): the validation half. It nullsAuthorityandConfigurationManagerand clearsRequireHttpsMetadata(:174-176) to stop OIDC/JWKS discovery outright, imports the public key (:178-183), and pins the token-validation parameters to that static key plus the fixed issuer and the caller's audience (:185-191). The doc explains why it has to exist (:158-167):AddForwardedJwtBearerotherwise fetches the issuer and signing keys from the Identity service's JWKS document through the gateway, which no in-process test topology serves.
- Public constants (
- Why it's built this way: the whole point is fidelity. Tokens are indistinguishable in shape and signing algorithm from production, so auth middleware and role checks are under test rather than stubbed. Splitting the key material into a mint side and a validate side is what lets a single-host fixture and a multi-host cross-service fixture share one committed keypair.
- Where it's used: tokens are applied to a client through
IntegrationTestBase<TFixture>'s
SetBearerToken(...)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:42-44) and wrapped by each app's role-specific token helpers.ConfigureInProcessTokenValidationis called from aPostConfigure<JwtBearerOptions>in the test factories of the non-Identity hosts, in ADC (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:49,77,.../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:56,86,.../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:50,69) and in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogTestWebApplicationFactory.cs:40,43,.../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesTestWebApplicationFactory.cs:47,57), plus the cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:50,.../EngagementCrossServiceFactory.cs:54,MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CatalogCrossServiceFactory.cs:50,.../SalesCrossServiceFactory.cs:52). It is covered directly byMMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/JwtTokenGeneratorTests.cs:39-95. - Caveats / not-in-source: the class doc
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/JwtTokenGenerator.cs:22-28) carries an explicit security warning, the embedded keypair is committed to the public git repo and is insecure by design, it exists only to make integration tests deterministic. Production keys are provisioned via user-secrets or Azure Key Vault perJwtSettings.RsaPrivateKeyPemand must never be this keypair.
MmcaGatewayHardeningTestsBase<TEntryPoint>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:39· Level 0 · class (abstract)
- What it is: the eight edge-hardening gates a gateway host must pass, authored once and re-run as a
thin sealed subclass per gateway. It covers what a host adopts from the shared gateway kit: the
per-client-IP rate limiter and its bypass list, an optional tighter named policy on the credential
route, the correlation ID stamped and echoed on every response, the per-downstream readiness checks,
and the active destination health probe on every cluster
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:15-22), which a host may declare off, in which case the same gate asserts it stays off. The subclass supplies only its own route-table facts. - Depends on: RecordingHttpForwarder (not referenced in code, but the
registration the base's doc expects a subclass to make,
:32-35), plusMicrosoft.AspNetCore.TestHost'sTestServer,WebApplicationFactory,Microsoft.Extensions.Diagnostics.HealthChecks'HealthCheckServiceOptions, YARP'sIProxyConfigProvider/IProxyConfigFilter,AwesomeAssertionsandXunit(:1-11). On the host side the behaviors it asserts come from the gateway kit: GatewayRateLimitingExtensions, GatewayCorrelationMiddleware, GatewayHealthCheckExtensions and GatewayHealthCheckDefaultsConfigFilter. - Concept introduced, driving the test server directly instead of through an
HttpClient. The rate limit assertions go throughTestServer.SendAsync(Action<HttpContext>, CancellationToken)rather than anHttpClient(:24-29), and the reason is a chain of two facts: the kit partitions onConnection.RemoteIpAddress, which aTestServerrequest leaves null, and the kit deliberately fails open on an unresolvable IP rather than collapsing every unattributable request into one shared bucket (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Gateway/GatewayRateLimitingExtensions.cs:238-244). A client-driven test could therefore never observe a 429. Setting the connection IP per request also gives each gate its own partition, which is what lets eight gates share one class fixture without disturbing each other.[Rubric §11, Security]and[Rubric §12, Performance & Scalability]are the limiter axis (ADR-019 for the service tier, ADR-088 for the edge tier this base checks);[Rubric §13, Observability & Operability]is the correlation ID;[Rubric §29, Resilience & Business Continuity]is the readiness and active-probe pair; and[Rubric §14, Testability]plus[Rubric §34, Architecture Governance & Documentation]cover the fitness-function form (ADR-015, ADR-058). - Walkthrough
- Client addresses (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MmcaGatewayHardeningTestsBase.cs:42-58): six constants, all inside the RFC 5737 TEST-NET-3 documentation range (203.0.113.0/24). Each gate burns its own address, so the fixture's shared host gives every one a fresh window (:28-29);ForwardedProxyIp(:58) stands in for the ingress proxy in the forwarded-header gate. - Abstract knobs, the only four a subclass must supply:
Factory(:61),PermitLimit(:68, stated by the subclass rather than read from the kit so an operator can see the number),LimitedPath(:71) andDownstreamServices(:77). - Virtual knobs with kit defaults:
BypassedPaths(:85-89,/.well-known/jwks.jsonand/health, exempt because probes and JWKS discovery run at high frequency by design),NamedPolicyPath(:95, null skips that gate) andNamedPolicyPermitLimit(:101),ActiveHealthChecksExpected(:111, defaulttrue;falsefits a host whose clusters each front one platform-balanced address such as a Container Apps internal FQDN, where ejecting the only destination can only turn a slow request into a 503, the platform already routes only to ready replicas, and every probe bills an otherwise idle downstream replica at the active rate,:103-110),ActiveProbeInterval(:114, 30 seconds) andActiveProbeTimeout(:117, 5 seconds),ActiveProbePath(:124,/aliveand not/health, because readiness on a downstream flips during its own rolling deployment and ejecting a destination for that is the gateway treating a healthy deploy as an outage,:119-123),CorrelationHeader(:127),DownstreamCheckPrefix(:130) andForwardedForHeader(:133). Route_IsThrottledOnceTheClientExhaustsItsWindow(:135-155): sends exactlyPermitLimitrequests, then one more, asserting none of the first batch was a 429 and the overflow was (:150-154).NamedPolicyRoute_IsThrottledAtItsOwnTighterAllowance(:157-191): returns early when the host declares no named policy (:162-165), a plain return rather than a declared dynamic skip because that would needxunit.v3.assert, which this shipped fixture library deliberately does not reference (:160-161). Otherwise it exhausts the tighter allowance, asserts the overflow is shed, and then asserts the same client on a route without the policy is still admitted (:185-190), which is what makes the rejection attributable to the route policy rather than the global limiter.BypassedPaths_AreNotThrottledEvenAfterTheWindowIsExhausted(:193-218): guards against an empty bypass list (:196-197), burns the window and proves it is exhausted (:205-206), then asserts every exempt path still answers with something other than 429.- The two correlation gates (
:220-236and:238-253): a request with no header must come back with a generated non-blank correlation ID, and a caller-supplied ID must be echoed unchanged, so a trace that started upstream survives the hop through the edge. Readiness_IncludesADownstreamCheckPerService(:255-281): reads the registeredHealthCheckServiceOptions(:262-263) and, per downstream, asserts a check named{DownstreamCheckPrefix}{service}exists, is taggedready, is not taggedlive, and hasFailureStatus = Unhealthy(:274-279). The comment is the reasoning: a gateway that cannot reach its services must leave the load balancer, but restarting the gateway process fixes nothing about a downstream outage, so the check must never fail liveness.EveryCluster_CarriesAnActiveHealthCheckProbingAlive(:283-324): resolves the proxy config and the registeredIProxyConfigFilterchain (:287-288), then runs each raw cluster through every filter (:296-302) because the provider hands out the pre-filter config and the kit's defaults only appear after the filters run, exactly as YARP's config manager does at load time. WhenActiveHealthChecksExpectedisfalseit asserts the opposite, that no cluster carries an enabled active check, and moves on (:308-313), so an active block left behind in configuration still fails the build. Otherwise it asserts each cluster carries an enabled active health check probingActiveProbePathat the declared interval and timeout (:315-322). The gate keeps its name in both modes.RateLimiter_PartitionsByForwardedClientIp_NotByProxyIp(:326-349): every request arrives from the same connection IP whileX-Forwarded-Fornames a different caller. It exhausts caller A's window, confirms the exhaustion (:339-340), then asserts caller B behind the same proxy IP is still admitted (:346-348), which only holds if the forwarded-headers middleware runs before the limiter.- Helpers:
SendAsync(path, clientIp)(:351-371) andSendForwardedAsync(path, proxyIp, forwardedFor)(:373-396), each shaping anHttpContexton the test server (method, HTTPS scheme, path, connection IP, and for the second one the forwarded header) and returning the response status code.
- Client addresses (
- Why it's built this way: the gates are identical across gateways because the kit is; the only
genuinely per-host facts are the route table and the configured numbers, so those are the four abstract
members and everything else is a virtual with a kit default. Asserting the effective YARP config
(post-filter) rather than the raw file is what keeps the health-probe gate honest about what the host
will actually do (ADR-089).
Turning the active probe off is a declared host fact rather than an absent assertion: the
ActiveHealthChecksExpectedswitch flips the gate to assert the probe stays off, so the choice cannot drift silently in either direction (:103-110,:308-313). - Where it's used: both gateways, each a sealed subclass over its own
Program. ADC (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:29) states a 120-request allowance (:40),/Events/1as the limited path (:43), four downstreams (:49-55),/hubsadded to the bypass list because a SignalR connection is long-lived (:57-69),/Auth/loginat 30 requests as its named policy (:72,:82), and declares active health checks off (MmcaGateway:HealthCheckDefaults:Active:Enabled=false) because each cluster fronts one Container Apps address and every probe bills an idle downstream replica (:84-89). Store (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:34) reads itsPermitLimitstraight out of the bound GatewayRateLimitingSettings options so the suite cannot drift from the section it exercises (:40-46), uses/Products/1(:49), three downstreams (:55), adds/Payments/webhookto the bypass list because a throttled Stripe webhook becomes a retry storm and an unreconciled payment (:57-70), declares/Auth/loginat 30 requests as its named policy (:73,:86; only the login and register submission routes carry it, because the UI replica refreshes sessions server-side with noX-Forwarded-Forand a client-IP partition there would key the whole storefront on one container address,:75-85), and declares active health checks off for the same one-address reason as ADC (:88-93). - Caveats / not-in-source: the gates need a booted gateway host, which MMCA.Common's own test project
does not have, so the framework can only guard the base's shape headlessly
(
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MmcaGatewayHardeningTestsBaseTests.cs:21): that all eight gate names are still declared as[Fact](:23-33,:43-54) and that exactly the four route-table members are still abstract (:35-41,:56-67), which keepsActiveHealthChecksExpecteda virtual, with an abstractSampleGatewayHardeningTests(:75, overridingActiveHealthChecksExpectedat:93) as compile coverage for the subclass surface.
OpenApiBreakingChangeKind
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4· Level 0 · enum
- What it is: the public vocabulary of OpenAPI breaking changes the conformance detector can report, thirteen
members, each documented in source
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4-44). - Depends on: nothing.
- Concept introduced, a closed taxonomy of contract breaks. Members group by what the baseline had:
paths and operations (
RemovedPath,RemovedOperation), responses (RemovedResponseStatusCode,RemovedResponseProperty), request bodies (RequestPropertyNewlyRequired,RemovedRequestBody,AddedRequiredRequestBody,RemovedRequestMediaType), parameters (RemovedParameter,ParameterNewlyRequired,AddedRequiredParameter,ChangedParameterType) and types (ChangedPropertyType).[Rubric §14, Testability]: a failing conformance run names a kind, not a vague diff. - Walkthrough: each
OpenApiBreakingChangecarries one kind plus a JSON pointer (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:479);OpenApiBreakingChange.csholds the record, and the detector tests assert specific kinds. - Why it's built this way: an enum keeps assertions stable and exhaustive; additions (a new request property that is optional, a new path) are intentionally absent because they do not break clients.
- Where it's used: the detector (13 references), its two test classes
(
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiBreakingChangeDetectorParameterTests.cs,.../OpenApiBreakingChangeDetectorTests.cs) andOpenApiBreakingChange.cs. ADR-058 (runtime conformance suites as a package) mentions it.
OperationSite
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:497· Level 0 · record (private sealed, nested)
- What it is: a nested record identifying one operation under comparison:
Path,Method,PathPointer, and the baseline and liveJsonElementpath items (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:497-508). - Depends on:
System.Text.Json.JsonElement. - Concept introduced, carrying location with the data. It computes
PointerasPathPointer + "/" + Method(:505) andDescribe(what)formats messages such asGET /Orders: query parameter 'page'(:507, upper-casing the method). - Walkthrough: created once per operation (
:102), then passed toCompareParameters(:260),CompareParameterSchema(:301) andCompareRequestBody(:326) so every finding is worded and pointed consistently. - Why it's built this way: a record bundles four values that always travel together, avoiding long parameter lists.
- Where it's used: only inside the defining file.
ParameterSite
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:511· Level 0 · record (private sealed, nested)
- What it is: a nested record for one parameter: location (
In),Name, the rawParameterelement and its JSONPointer(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:511-518). - Depends on:
JsonElementand the detector'sHasRequiredFlaghelper (:204). - Concept introduced, effective required-ness.
IsRequiredis true for anypathparameter regardless of its own flag, otherwise it follows the declared flag (:513-515);Describe()yields{In} parameter '{Name}'(:517). - Walkthrough:
EffectiveParametersbuilds a dictionary keyedlocation|name, case-insensitive (:224-254), soCompareParameterscan detect removed, newly required and added-required parameters (:260), andCompareParameterSchemacompares types (:301). - Why it's built this way: OpenAPI mandates path parameters be required, so trusting only the flag would miss a break.
- Where it's used: only inside the defining file.
RateLimiterTestExtensions
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:11· Level 0 · class (static)
- What it is: a one-member helper that replaces a test host's global rate limiter with an
unlimited partition, so a suite that drives dozens of requests through one host in a few seconds is not
throttled by the production budget
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:15-19). - Depends on: BCL and ASP.NET Core only,
System.Threading.RateLimiting,Microsoft.AspNetCore.RateLimiting'sRateLimiterOptions,Microsoft.AspNetCore.Httpand the DI abstractions (:1-4). No first-party dependency. On the host side, the limiter it neutralizes is the one configured by RateLimitingSettings (ADR-019). - Concept introduced, neutralize the budget without removing the middleware. Two design points make
this safe to apply everywhere (
:20-25). First it runs as aPostConfigure, so it wins over whatever the host registered no matter when the host's ownAddRateLimiterran, which matters because a test factory'sConfigureTestServicescan execute before or after the host's registration. Second only the global limiter is replaced: the limiter middleware stays in the pipeline, so MiddlewarePipelineOrderTestsBase still sees theRateLimitingstep, and per-endpoint named policies are untouched, which is what lets a suite like MmcaGatewayHardeningTestsBase<TEntryPoint> still assert a named policy engages.[Rubric §14, Testability]assesses whether a production safety control can be stood down for a test without deleting it;[Rubric §12, Performance & Scalability]is the control itself. - Walkthrough
- The class body is one C# preview
extension(IServiceCollection services)block (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RateLimiterTestExtensions.cs:13), the same extension-member style the framework uses for DI registration (see primer §4). NeutralizeGlobalRateLimiter(string partitionName = "tests")(:32-39): a singlePostConfigure<RateLimiterOptions>that setsGlobalLimiterto aPartitionedRateLimiter.Create<HttpContext, string>returningRateLimitPartition.GetNoLimiter(partitionName)for every request (:34-36), then returns the collection for chaining (:38).partitionNameonly ever surfaces in limiter diagnostics (:27-30), which is why the call sites use it as a label ("integration-tests","cross-service-tests") rather than for behavior.
- The class body is one C# preview
- Why it's built this way: a production per-IP or per-user budget and a test suite firing its whole
arrange phase through one in-process host are simply incompatible, and the alternatives are worse:
removing the middleware would break the pipeline-order fitness test, and raising the configured limit
would leave the suite silently coupled to a number in
appsettings. - Where it's used: eight call sites, all in ADC, four per-service test factories
(
MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceTestWebApplicationFactory.cs:63,.../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementTestWebApplicationFactory.cs:72,.../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityTestWebApplicationFactory.cs:47,.../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationTestWebApplicationFactory.cs:57) and four cross-service factories (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/ConferenceCrossServiceFactory.cs:55,.../EngagementCrossServiceFactory.cs:59,.../IdentityCrossServiceFactory.cs:36,.../NotificationCrossServiceFactory.cs:63). MMCA.Common covers all three behaviors: that it overrides a host limiter which would otherwise reject every request (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RateLimiterTestExtensionsTests.cs:21), that it returns the same collection for chaining (:48), and that it still applies when the host configures its limiter afterwards (:61). - Caveats / not-in-source: no MMCA.Store or MMCA.Helpdesk test project calls it today; a
workspace-wide search finds no
NeutralizeGlobalRateLimiterreference under either repo'sTests/tree.
SecurityHeadersTestsBase
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:16· Level 0 · class (abstract)
- What it is: a one-test conformance base that asserts a booted host emits the hardened set of security response headers on every response, so a later pipeline refactor cannot silently drop them. Authored once, re-run as a thin subclass per host under test.
- Depends on:
AwesomeAssertionsandXunit(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:1-2). It deliberately does not extend IntegrationTestBase<TFixture>: it needs only anHttpClient, so it takes one through an abstract factory rather than inheriting the SQL fixture machinery. - Concept: a runtime conformance check on the HTTP edge.
[Rubric §11, Security]and[Rubric §26, Front-End Security]both assess defense in depth at the edge; this test pins the exact header values the sharedAddCommonSecurityHeaders/UseCommonSecurityHeadersmiddleware (see SecurityHeadersMiddleware, ADR-023) is expected to emit.[Rubric §14, Testability]covers the reusable-base shape. - Walkthrough
ProbePath(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:19): overridable, defaults to/alivebecause the liveness endpoint always answers independent of any backend being reachable, so the header check is never flaky for the wrong reason (rationale in the class doc,:12-14).AliveResponse_CarriesHardenedSecurityHeaders(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:21-36): the single[Fact]. It GETsProbePath(:26-27, threadingTestContext.Current.CancellationToken) and asserts six headers (:29-35):X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, aPermissions-Policycontaininggeolocation=(), aContent-Security-Policycontainingframe-ancestors 'none', and (because the host under test boots in the Production environment) an HSTSStrict-Transport-Securityheader with amax-age=.CreateClient()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/SecurityHeadersTestsBase.cs:42): abstract, the subclass supplies it from itsWebApplicationFactoryclass fixture.Header(...)(:44-45) is the private helper that joins a header's values or returns null when the header is absent, which is what makes a missing header fail with a readable null-versus-expected message.
- Why it's built this way: pinning literal header values (not just presence) turns "we harden
responses" into an executable, per-host guarantee, and probing
/alivekeeps the test independent of application state. Booting the subclass fixture in Production is what makes the HSTS assertion valid, which is why the two adopters pair it with ProductionHostApplicationFactory<TEntryPoint>. - Where it's used: both gateway hosts subclass it with a single
CreateClientoverride,MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12andMMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13, each also taking aProductionHostApplicationFactory<Program>as its xUnit class fixture on the same declaration.
TestPolling
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:9· Level 0 · class (static)
- What it is: a poll-with-timeout helper for asynchronous integration assertions. It repeatedly probes a value until a condition holds or a budget runs out, and hands back the last probed value either way.
- Depends on: BCL only (
Task,DateTime,TimeSpan). No first-party dependency and no assertion library, so the caller keeps ownership of the assertion. - Concept introduced, replacing the pre-assert sleep. Anything that travels the outbox to a broker and
back, or any other eventually-consistent path, arrives at a time the test cannot know
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:3-8). A fixedTask.Delaybefore the assertion is both slow and flaky: too short and the suite reds intermittently, too long and every green run pays the worst case. Polling returns as soon as the condition holds and bounds the wait.[Rubric §14, Testability]assesses whether the suite is deterministic;[Rubric §6, CQRS & Event-Driven]is why the problem exists at all, since the outbox (ADR-003) is asynchronous by design and offers no synchronous handle to await. - Walkthrough
PollUntilAsync<T>(Func<Task<T>> probe, Func<T, bool> isSatisfied, TimeSpan? timeout = null, TimeSpan? interval = null)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/TestPolling.cs:22-41): null-guards both delegates (:28-29), computes a deadline from the 60-second default budget (:31) and a 500 ms default interval (:32), probes once before the loop (:33), then loops while the condition is unmet and the deadline has not passed (:34-38).- The return is the design decision worth noticing: it returns
lastunconditionally (:40) rather than throwing on timeout, so a timed-out poll still fails on the caller's real assertion message rather than on a bare timeout exception (the doc states exactly this at:11-14).
- Why it's built this way: bounding the wait and returning the last value keeps two properties at once, a fast green path (the loop exits on the first satisfying probe) and a diagnosable red path (the failure message describes the domain expectation, not the plumbing).
- Where it's used: the cross-service tiers of both apps route every eventual assertion through it and
say so in their own docs
(
MMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs:15and the ADC equivalent atMMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceTestBase.cs), with call sites such asMMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/CrossService/ProductVariantChangedRoundTripTests.cs:28,48,51. MMCA.Common covers the helper itself, including the null-argument guards (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/TestPollingTests.cs:15-64).
CrossServiceFixtureBase
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:47· Level 1 · class (abstract)
- What it is: the shared scaffolding for the cross-service real-broker integration tier. It boots
several service hosts in ONE process against a real Testcontainers SQL Server and a real Testcontainers
RabbitMQ, so the genuine outbox to broker to consumer round-trip (and any real cross-service gRPC read)
is exercised end to end rather than faked
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:17-25). - Depends on: CrossServiceDataSource (the per-source declaration), plus
Microsoft.Data.SqlClient,Testcontainers.MsSql,Testcontainers.RabbitMq, and xUnit'sIAsyncLifetime(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:1-4,47). - Concept introduced, the multi-host in-process topology and its configuration channel. Where
SqlServerIntegrationTestFixtureBase<TEntryPoint> boots
one host with the cross-service edges faked and no broker, this base owns a whole topology. Two
mechanisms are load-bearing, and both are documented on the class. First, process environment
variables are the only override channel these hosts honor
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:26-39): each host reads its connection string,MessageBussettings, and JWT settings frombuilder.Configurationat configure-time, beforebuilder.Build(), which is beforeWebApplicationFactory.ConfigureAppConfigurationdeltas apply, so in-memory config would arrive too late. Second, because the one genuinely per-host key is the SQL connection string, hosts must boot strictly sequentially, and that is safe precisely because a booted host has already snapshotted its connection (the data-source resolver, the context factory, the outbox processor, and the MassTransit bus are all built duringStartAsync).[Rubric §7, Microservices Readiness]assesses whether extracted services really do collaborate over their declared transports;[Rubric §6, CQRS & Event-Driven]covers the outbox path (ADR-003);[Rubric §8, Data Architecture]covers database-per-service (ADR-006); and[Rubric §14, Testability]covers shipping the whole topology as a reusable base. - Walkthrough
- State: the private
DummyBearerAuthorityconstant (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:51), the original-environment snapshot map (:47), the two nullable containers (:49-50), and the publicRabbitMqConnectionString(:53). - Subclass knobs:
DataSources(:60, the logical sources in the order their databases are created),MigrationsAssemblyPrefix(:67, so each named source gets{prefix}.{LogicalName}exactly as production does),BootHostsAsync(:143) andDisposeHostsAsync(:146), plus the optional hooksOnContainersStartedAsync(:152) andConfigureSharedEnvironment(:161). SqlServerBaseConnectionString(:70-72) throws a clearInvalidOperationExceptionwhen the container has not started, and thepublic static ComposeConnectionString(...)(:82-96) overlays a catalog,TrustServerCertificate, and an optional Application Name. It is deliberately pure and static so a fixture's connection-string composition is unit-testable with no Docker daemon (:74-81);BuildConnectionString(:168-169) is the instance shorthand over it.InitializeAsync(:99-116) is the whole lifecycle in seven steps: create the containers, start both in parallel withTask.WhenAll(:102), read the broker connection string (:104), run the subclass hook (:105), pre-create the databases (:111), push the shared environment (:113), then boot the hosts (:115). The pre-create step carries the sharpest comment in the file (:107-110):CREATE DATABASEruns before EF's migration lock (sp_getapplock) is acquired, so a host booted twice (the real-Kestrel double-boot pattern) would otherwise race itself; with the databases already present EF skips the create and the migration lock serializes the actual migration run.SetSharedEnvironment(:227-258) pushesASPNETCORE_ENVIRONMENT=Testing(:229), one named data source per module (:243-246), the real broker settingsMessageBus__Provider=RabbitMqandConnectionStrings__rabbitmq(:249-250), and the dummy Bearer authority that only has to exist soAddForwardedJwtBearer's authority guard passes (:255; real validation is re-pointed at the committed test key by JwtTokenGenerator.ConfigureInProcessTokenValidation,:252-254), before handing control to the subclass (:257).- The named-source loop is the multi-host fix, and its comment (
:231-242) is worth reading in full: EF Core caches a context type's model in a process-global cache keyed by (context type, source name). If every host let its entities collapse ontoDefault(as production does, one host per process), all hosts here would share ONE cached model and the first booted would win.SetNamedDataSource(:265-276) therefore composes each module's connection string with a distinct Application NameMMCA-{LogicalName}(:270) so it differs ordinally from the top-levelConnectionStringsvalue and the resolver keeps the named source, giving each host its own model-cache key. SetHostConnectionString(:177) is the one key mutated between sequential boots, andSetEnvironmentVariable(:187-195) records only the first original value per key so re-pushing that key cannot clobber the restore point.DisposeAsync(:119-136) disposes the hosts, then RabbitMQ, then SQL, then callsRestoreEnvironment(:278-286).CreateContainers(:203-204) is built inside a method rather than a field initializer so a subclass can be constructed and its non-container logic unit-tested on a machine with no Docker daemon, and it carries a documentedCS0618suppression for the parameterless Testcontainers module builders (:197-202).CreateDatabasesAsync(:207-225) issues one guardedIF DB_ID(...) IS NULL CREATE DATABASEper source with a scopedCA2100suppression justified because the database names are the subclass's own compile-time constants (:218).
- State: the private
- Why it's built this way: the environment-variable channel and the sequential boot are not style choices, they are the only way to give several hosts distinct databases in one process given configure-time configuration reads. Everything genuinely per-app (which databases, how many hosts and in what order, which extra settings) stays behind the four abstract members.
- Where it's used: one fixture per app, both sealed subclasses,
MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:24(three databases for three REST hosts at:61-66, migrations prefixMMCA.ADC.Migrations.SqlServerat:69) andMMCA.Store/Tests/Integration/MMCA.Store.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:29(two databases at:56-60, prefixMMCA.Store.Migrations.SqlServerat:63). MMCA.Common covers the container-free half of the base through its own privateFakeCrossServiceFixture(MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/CrossServiceFixtureBaseTests.cs:119). - Caveats / not-in-source: this tier needs a Docker daemon. Where each repo schedules it is a CI decision recorded outside this class; the base itself says nothing about scheduling. It also proves the round-trip over RabbitMQ, not the production transport, which is what ServiceBusEmulatorFixtureBase exists to cover.
DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:38· Level 1 · class (abstract)
- What it is: an opt-in fitness function that builds a real
ServiceCollectionthrough a repo's own registration sequence, resolves the decorated command and query handlers out of the built provider, and asserts the runtime object graph nests the decorators in exactly the ADR-014 order. - Depends on:
ICommandHandler<in TCommand, TResult>
and IQueryHandler<in TQuery, TResult> from
MMCA.Common.Application.UseCases(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:4), plusSystem.Reflection,Microsoft.Extensions.DependencyInjection,AwesomeAssertions, andXunit(:1-5). - Concept introduced, verifying a decorator chain by unwrapping the constructed graph. The decorator
pipeline itself is taught in
group-05; what is new here is how
you prove it. Scrutor's
TryDecorateapplies decorators in reverse registration order, so the outermost decorator is the last one registered, and an innocent-looking reorder of theAddApplicationDecorators()lines (or a module scan that runs after it) silently changes runtime behavior with no compile error (class doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:16-19). Rather than inspecting the registration list, this base resolves the service and walks the real chain by reflection (:29-32).[Rubric §6, CQRS & Event-Driven]assesses whether the command/query pipeline is coherent and intentional;[Rubric §2, Design Patterns]assesses correct application of the decorator pattern;[Rubric §14, Testability]covers turning an ordering convention into an executable check; and[Rubric §34, Architecture Governance & Documentation]covers the fact that a decision record is enforced here rather than merely written down (ADR-015 is the general case). It is also the one non-HTTP member of the ADR-058 conformance tier. - Walkthrough
- Four type parameters
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:34-37): a representative command with itsTResultand a representative query with itsTResult, each of which must have a concrete registered handler. ConfigureServices(IServiceCollection services)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/DecoratorPipelineOrderTestsBase.cs:46): the one abstract member. The subclass registers test doubles for the decorator dependencies (IFeatureManager, ICurrentUserService, IPermissionRegistry, ICorrelationContext, ICacheService, IUnitOfWork,ILogger<>) and then runs the repo's real registration sequence, module scans first andAddApplicationDecorators()last (doc:20-28).ExpectedCommandDecorators(:49-58) pins seven links, outermost first: FeatureGateCommandDecorator<TCommand, TResult>, AuthorizationCommandDecorator<TCommand, TResult>, LoggingCommandDecorator<TCommand, TResult>, CachingCommandDecorator<TCommand, TResult>, ValidatingCommandDecorator<TCommand, TResult>, TimeoutCommandDecorator<TCommand, TResult>, TransactionalCommandDecorator<TCommand, TResult>.ExpectedQueryDecorators(:61-69) pins six: FeatureGateQueryDecorator<TQuery, TResult>, AuthorizationQueryDecorator<TQuery, TResult>, LoggingQueryDecorator<TQuery, TResult>, CachingQueryDecorator<TQuery, TResult>, ValidatingQueryDecorator<TQuery, TResult>, TimeoutQueryDecorator<TQuery, TResult>, the query pipeline differing from the command one by the absence of the transaction only. Both lists arevirtual, so a host with a deliberately different chain can narrow them.- The two
[Fact]s,CommandPipeline_NestsDecorators_InAdr014Order(:71-73) andQueryPipeline_NestsDecorators_InAdr014Order(:75-77), each hand the closed handler interface and the expected list toAssertPipeline. AssertPipeline(:79-98): builds the collection, builds a provider, opens a scope (handlers are scoped,:84-85), resolves the outermost handler and asserts it is non-null with a message that tells the subclass author what is missing (:87-89). It then unwraps the chain, maps each link to a simple type name, and asserts every element except the last equals the expected decorator list in order (:91-94), finally asserting the innermost element does not end inDecorator, that is, it is the concrete handler (:96-97).UnwrapChain(:105-125): walks outermost to innermost by reflecting over each object's instance fields (public and non-public) and picking the first value that implements the same closed handler interface and is not the object itself (:112-115), which is how it finds the compiler-generated backing field holding the inner handler.SimpleTypeName(:127-132) strips the generic-arity backtick suffix so a two-arityLoggingCommandDecoratorcompares as the plain name.
- Four type parameters
(
- Why it's built this way: asserting the constructed object graph is strictly stronger than asserting
the registration list, it catches a decorator that was registered but never applied (for example
because a module scan re-registered the handler afterwards). Comparing simple type names keeps the base
free of a compile-time reference to the decorator classes, which live in
MMCA.Common.Application. - Where it's used: four subclasses today. MMCA.Common self-tests the base against its own
registration sequence with a synthetic
PingCommand/PingQuerypair (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/DecoratorPipelineOrderTests.cs:24), and the three applications subclass it in their architecture tiers,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:30andMMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DecoratorPipelineOrderTests.cs:30over the real IdentityChangePreferencesCommand/GetUserPreferencesQuerypair, andMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:35over the Tickets pair. - Caveats / not-in-source: each subclass pins one representative command/query pair, not every handler, so the guard proves the ordering is right, not that every handler is decorated.
ProductionHostApplicationFactory<TEntryPoint>
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23· Level 0 · class
- What it is: the database-free boot path for host-level tests. It is a
WebApplicationFactorythat pins the hosting environment toProductionand hangs on to the startedIHost, so a test can both exercise production-only middleware branches and drive the host's own lifetime. - Depends on:
Microsoft.AspNetCore.Mvc.Testing'sWebApplicationFactory<TEntryPoint>(extended,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:23) andMicrosoft.Extensions.Hosting'sIHost/IHostBuilder(ProductionHostApplicationFactory.cs:1-2). No first-party dependency. - Concept introduced, the second boot path. The integration tier has two ways to get a running host:
SqlServerIntegrationTestFixtureBase<TEntryPoint>
for hosts that need a real database, and this one for hosts that do not (a YARP reverse-proxy gateway
is the usual case,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:17-20). Both are named as the two paths in ADR-058.[Rubric §11, Security]is the reasonProductionis pinned: the restrictive CORS policy, HSTS emission, and other production-only middleware are branches a defaultDevelopmentboot skips entirely, which is exactly where host misconfiguration hides (ProductionHostApplicationFactory.cs:10-13).[Rubric §14, Testability]covers the second half, capturing the host is what makes a lifetime test possible at all. - Walkthrough
StartedHost(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:30): a public property with a private setter, nullable becauseWebApplicationFactorybuilds its host lazily, so it stays null until the first client is created (:25-28).CreateHost(IHostBuilder builder)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/ProductionHostApplicationFactory.cs:33-40): null-guards the builder (:34), callsbuilder.UseEnvironment("Production")(:36), then assigns and returnsbase.CreateHost(builder)(:37-38). Three lines of override, and the assignment is the entire reason the class exists.
- Why it's built this way:
IHost.StopAsyncis not reachable through theWebApplicationFactorysurface alone (ProductionHostApplicationFactory.cs:13-15), so a graceful-shutdown test has no handle to pull without this capture. The class is deliberately left unsealed and non-abstract so it can be used directly as an xUnitIClassFixture<...>with no subclass. - Where it's used: as the default factory of
GracefulShutdownTestsBase<TEntryPoint>
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), directly as the class fixture of both gateway security-header tests (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/SecurityHeadersTests.cs:11-12,MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12-13), and as the base of ADC's gateway-hardening fixture, which subclasses it only to swap in the recording forwarder (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:96-105). - Caveats / not-in-source: the doc is explicit that a host which migrates or seeds on startup needs
its own fixture (
ProductionHostApplicationFactory.cs:17-20); this factory does nothing about a database.
HostScopedAssemblyProvider
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:116· Level 1 · class (private sealed, nested)
- What it is: a private nested decorator over
IEntityConfigurationAssemblyProviderthat narrows the assemblies a host scans for entity configurations to the ones that host would load in its own process. It is the working half ofCrossServiceHostIsolation.IsolateCrossServiceHost<TEntryPoint>(), the extension that CrossServiceFixtureBase subclasses call fromConfigureTestServicesof every host factory (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:10-13,42). - Depends on:
IEntityConfigurationAssemblyProvider(the interface it implements and wraps),EntityConfigurationOptions.AdditionalAssemblies(the explicit per-host registrations it keeps), andSystem.Reflection.Assembly(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:1-6,116-119). - Concept introduced, scoping a shared AppDomain back to one host. The default provider scans
AppDomain.GetAssemblies, and with several hosts in one process that returns every host's module assemblies. Each host's entity registry then claims its peers' data sources, so its outbox and internal-command processors drain the PEER databases and a row whose handler lives only in the peer is dead-lettered ashandler_missingby the wrong host (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:14-21). The provider restores the one-service-per-process view.[Rubric §7, Microservices Readiness]assesses whether extracted services behave as independent hosts;[Rubric §8, Data Architecture]covers database-per-service (ADR-006);[Rubric §14, Testability]covers keeping the in-process topology faithful to deployment. - Walkthrough
- The primary constructor takes the wrapped
innerprovider, thehostAssemblyNamesset, and theexplicitAssembliescollection (:116-119). GetConfigurationAssemblies()(:121-124) runs the inner provider and keeps an assembly when it is inexplicitAssembliesor its simple name is inhostAssemblyNames; the result is materialized with a collection expression.- The name set is not built here.
ReadHostAssemblyNames(:79-114) reads every runtime library the host's.deps.jsonlists plus the host assembly itself, and throwsInvalidOperationExceptionwhen the file is missing beside the assembly (:86-91).IsolateCrossServiceHostremoves the existing non-keyed registrations (:47-53), reuses the last asinner(:55), and registers this class as a singleton (:56-59);CreateInner(:65-73) rebuilds the original from its descriptor, falling back toDefaultEntityConfigurationAssemblyProvider.
- The primary constructor takes the wrapped
- Why it's built this way: decorating rather than replacing keeps whatever provider the host registered
and only narrows its result. Explicit
AdditionalAssembliesbypass the filter because that registration is already per host (:23-28). - Where it's used: only inside its defining file, via
IsolateCrossServiceHost<TEntryPoint>()(:56). No other site constructs it. Not determinable from source here: which consumer fixtures call the extension (the brief lists no usage sites).
InMemoryQueryableExecutor
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:16· Level 1 · class
- What it is: a no-op, in-memory stand-in for the framework's
IQueryableExecutor, the interface that carries the
EF Core-specific parts of query execution (eager loading, split queries, materialization) out of
application code. This implementation lets a unit test build the same query pipeline against a plain
IQueryable<T>(an in-memory list, typically) with no EF Core provider in play at all. - Depends on: IQueryableExecutor
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:16), the interface it implements, plus BCL LINQ only (System.Linq). No EF Core reference. - Concept introduced, the null-object executor.
[Rubric §14, Testability]assesses how cheaply application code can be exercised without its infrastructure; a handler or service that takesIQueryableExecutoras a dependency stays unit-testable because a test can hand it this no-op instead of standing up a realDbContext. Every member either returns the query unchanged or runs the equivalent synchronous LINQ operator, so the executor adds no behavior of its own, it exists purely to satisfy the interface's shape over a query that is already fully in memory. - Walkthrough
Include<T>(IQueryable<T> query, string navigationPropertyPath)(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryQueryableExecutor.cs:19-21): returnsqueryunchanged, an in-memory collection has no navigation properties left to load.AsSplitQuery<T>(IQueryable<T> query)(:24-26): likewise a pass-through, split-query behavior is an EF Core query-translation concern with nothing to do once the sequence is already materialized.ToListAsync<T>(IQueryable<T> query, CancellationToken cancellationToken = default)(:29-30): wraps the synchronousquery.ToList()inTask.FromResult, so anawaitat the call site still works with no real asynchrony underneath.CountAsync<T>(IQueryable<T> query, CancellationToken cancellationToken = default)(:32-33): the same pattern overquery.Count().
- Why it's built this way: keeping every member a one-line pass-through or a synchronous LINQ call
wrapped in
Task.FromResultis what makes the type safe to reuse across every test that needs anIQueryableExecutor, there is no state to reset between tests and no behavior that could diverge from what an in-memoryIQueryable<T>already does on its own. - Where it's used: as the
IQueryableExecutorhanded to application-layer tests across both apps and the framework itself, for exampleMMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/Helpers/TestSupport.cs,.../SessionQuestions/UseCases/GetModerationQueueHandlerTests.cs,.../SessionQuestions/UseCases/GetSessionQuestionsHandlerTests.cs,.../LivePolls/DomainEventHandlers/LivePollVoteChangedHandlerTests.cs,.../LivePolls/Services/LivePollResultsBuilderTests.cs,.../LivePolls/UseCases/CastVoteHandlerTests.cs,.../LivePolls/UseCases/GetOpenPollsHandlerTests.cs,.../LivePolls/UseCases/GetPollResultsHandlerTests.cs,.../SessionQuestions/UseCases/SubmitQuestionHandlerTests.cs,MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/UserNotificationExportServiceTests.cs,MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Services/Query/EntityQueryServiceTests.cs, and.../Services/Query/EntityQueryPipelineOrderingTests.cs, plus one more site not shown in the brief's usage-site slice.
IntegrationTestBase<TFixture>
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:13· Level 1 · class (abstract)
- What it is: the workhorse base class every integration test inherits. It owns the per-test HTTP client and lifecycle, typed request helpers, bearer-token management, and a thread-safe id counter, so a concrete test class is left with just its arrange/act/assert.
- Depends on: IIntegrationTestFixture (the
TFixtureconstraint,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:14), plusXunit'sIAsyncLifetime,System.Net.Http.Headers, andSystem.Net.Http.Json(:1-3). - Concept introduced, the xUnit async test lifecycle and per-test isolation.
[Rubric §14, Testability]: the base implementsIAsyncLifetimesoInitializeAsyncruns before each test andDisposeAsyncafter, and it hangs the database reset off that hook so every test starts from a clean database, the single most important property for reliable integration tests. - Walkthrough
- Fields and properties: a
static int _nextId = 1000seed (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/IntegrationTestBase.cs:16), and theFixture/Clientprotected properties (:19-22). - Constructor (
:24-28): stores the injected fixture and eagerly creates theHttpClientfrom it. InitializeAsync(:31): aValueTaskthat awaitsFixture.ResetDatabaseAsync()before each test.DisposeAsync(:34-39): suppresses finalization and disposes the client.- Auth helpers:
SetBearerToken(string)/ClearAuthentication()(:42-48) set or clear theAuthorizationheader, the hook through which a JwtTokenGenerator token is applied. - Typed HTTP helpers:
GetAsync<T>(:51-56, which callsEnsureSuccessStatusCodethen deserializes), andPostAsync<T>/PutAsync<T>/PutAsync/DeleteAsync(:59-72) returning the rawHttpResponseMessageso a test can assert status codes. The four write helpers are plain expression-bodied pass-throughs returning the client'sTaskdirectly (noasync/awaitwrapper), whileGetAsync<T>staysasyncbecause it awaits twice. NextId()(:75):Interlocked.Incrementover the shared seed, so parallel tests never collide on generated ids.
- Fields and properties: a
- Why it's built this way: per-test database reset plus a per-test client is the isolation contract;
centralizing the typed helpers keeps individual tests short and consistent. The static
Interlockedcounter is the cheapest safe way to hand out unique ids under xUnit's parallelism. Note the asymmetry:GetAsync<T>throws on a non-success status while the others hand the response back, which is what makes a read helper terse and a write assertion explicit. - Where it's used: the direct base of all three contract test bases in this unit (OpenApiContractTestsBase<TFixture>, ProblemDetailsContractTestsBase<TFixture>, ServiceInfoVersioningContractTestsBase<TFixture>), and of every concrete integration test in the downstream apps.
OpenApiBreakingChange
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChange.cs:12· Level 1 · sealed record
- What it is: one finding of the OpenAPI breaking-change gate: a OpenApiBreakingChangeKind plus the JSON pointer of the place in the baseline document that broke, with an optional human-readable
Detail(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChange.cs:12). - Depends on:
OpenApiBreakingChangeKind(the positionalKindparameter,:12) and nothing else; it is a plain value. - Concept introduced, a finding as a value record.
[Rubric §9, API & Contract Design]: a sealed positional record gives value equality for free, which is exactly what lets the detector collect findings in aHashSet<OpenApiBreakingChange>(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:73) and report a shared component once instead of once per reference site. - Walkthrough
Detail(:19): aninit-only nullable string. Parameter and request-body findings fill it with the method, path and subject (for exampleGET /Orders: query parameter 'page'); the other kinds leave itnull.ToString()(:26):"{Kind} {JsonPointer}", or"{Kind} {JsonPointer} ({Detail})"when there is a detail, so the contract test can print one line per change (for exampleRemovedPath /paths/~1Orders).
- Why it's built this way:
Detailsits outside the positional parameters on purpose, so it takes part in equality only through the record's synthesized members while two findings at one pointer with different details stay distinct. The pointer alone does not name a parameter site well, hence the extra text. - Where it's used: produced by OpenApiBreakingChangeDetector and rendered by
OpenApiContractTestsBase<TFixture>'s baseline test (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:157).
RecordingHttpForwarder
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:29· Level 1 · sealed class
- What it is: an
IHttpForwarderthat never proxies. It echoes the destination prefix, the matched cluster, and theForwarderRequestConfigit was handed into response headers, so a gateway test can assert which cluster a route targets and which forwarder budget it carries (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:8-13). - Depends on: YARP only,
Yarp.ReverseProxy.Forwarder(IHttpForwarder,ForwarderRequestConfig,HttpTransformer,ForwarderError) andYarp.ReverseProxy.Model(IReverseProxyFeature), plusMicrosoft.AspNetCore.HttpandSystem.Globalization(:1-4). No first-party dependency. The trace headers it reads back are the ones the gateway kit's GatewayTraceHeaderTransformProvider stamps. TheYarp.ReverseProxypackage reference lives inMMCA.Common.Testingitself rather than in every gateway test project, and the project file records why (MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:43-46). - Concept introduced, the echoing fake at a framework extension point. Three ideas are worth taking
from this type. First, echoing into response headers rather than storing state is what keeps a
singleton fake safe for concurrent requests (
:12-13): there is no shared mutable field to race. Second, replacing the DI singleton still intercepts under a config-driven route table, because YARP's own forwarder middleware resolvesIHttpForwarderfrom DI, soMapReverseProxy()goes through the fake exactly as a hand-writtenMapForwardercall would (:14-22). Third, it runs the real transform pipeline against a throwaway outbound request before echoing (:23-27), so the request transforms are observable without a network hop, which is what makes an assertion about what a downstream would actually receive meaningful.[Rubric §2, Design Patterns]covers the test-double form (a recording fake, not a mock);[Rubric §14, Testability]covers keeping every gateway test off the network, since configured destinations are usually service-discovery names that resolve to nothing in a test host; and[Rubric §7, Microservices Readiness]is the topology this makes testable (ADR-089). - Walkthrough
- Header constants (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/RecordingHttpForwarder.cs:32-54):DestinationHeader,ClusterHeader,ActivityTimeoutHeader,VersionHeader,VersionPolicyHeader, plusRouteTraceEchoHeaderandClusterTraceEchoHeaderfor the two trace headers read back off the transformed outbound request.UnsetValue(:57) is the(unset)marker written when the forwarder config leaves a nullable setting unset, so a test asserts a value either way rather than distinguishing an absent header from an unset setting. RouteTraceHeaderName(:63) andClusterTraceHeaderName(:69):initproperties defaulting to the shared kit's own header names,X-MMCA-RouteandX-MMCA-Cluster, so a host with different header names can still use the fake.- The five-argument
SendAsyncoverload (:72-78) simply delegates to the six-argument one withCancellationToken.None. - The real
SendAsync(:81-115): null-guards context, config and transformer (:89-91); writes the destination prefix (:93); reads the matched cluster id offIReverseProxyFeature, falling back toUnsetValue(:94-95); writes the activity timeout, forwarded HTTP version and version policy (:96-101); runstransformer.TransformRequestAsyncagainst a throwawayHttpRequestMessage(:106-108); echoes the two stamped trace headers (:110-111); then sets a 200 and returnsForwarderError.None(:113-114). HeaderOrUnset(:121-124): reads one outbound header, joining multiple values with a comma, or returns the unset marker.
- Header constants (
- Why it's built this way: a gateway test wants to assert routing decisions and forwarder budgets, not the behavior of a backend. Swapping the forwarder is the smallest replacement that leaves the entire gateway pipeline (routing, transforms, limiter, correlation, health) genuinely running, which is what makes the same fixture reusable for both the route-map assertions and the hardening gates.
- Where it's used: both gateway test projects register it by replacing the singleton. ADC uses it in
the gateway-hardening fixture
(
MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GatewayHardeningTests.cs:103) and in the route-map suite, which aliases all seven header constants at the top of the class (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/RouteMapTests.cs:42-60, registration at:430); Store does the same in both places (MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GatewayHardeningTests.cs:115,MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/RouteMapTests.cs:378, with the rationale written out at:21). MMCA.Common covers the fake itself, including the unset-marker paths and the custom trace header names (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/RecordingHttpForwarderTests.cs:17).
GracefulShutdownTestsBase<TEntryPoint>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:25· Level 1 · class (abstract)
- What it is: a shutdown conformance base. It boots a real host, calls a real
IHost.StopAsyncunder a bounded cancellation token, and asserts the host drained cleanly, firingApplicationStoppingand thenApplicationStoppedinside the timeout. - Depends on:
ProductionHostApplicationFactory<TEntryPoint>
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:32), plusMicrosoft.Extensions.Hosting'sIHost/IHostApplicationLifetime,Microsoft.Extensions.DependencyInjection,AwesomeAssertions, andXunit(:1-4). - Concept introduced, the bounded-stop drain check.
[Rubric §29, Resilience & Business Continuity](named in the class doc itself,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:10) assesses whether the system survives planned and unplanned interruption; a rolling deploy is the planned one. The failure this catches is a hosted service (a warm-up runner, service discovery, proxy infrastructure) that refuses to drain, which in production does not announce itself: it silently wedges a rolling deploy while the platform waits out its termination grace period (:13-17).[Rubric §13, Observability & Operability]is the operational half, lifetime events firing in order are what a platform's shutdown handling depends on. The recovery-objective framing is ADR-009; the base itself is one of the suites recorded in ADR-058. - Walkthrough
ShutdownTimeoutSeconds(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/GracefulShutdownTestsBase.cs:29):virtual, defaults to 20 seconds. This number is the test: a host that drains slower than this fails.CreateFactory()(:31):virtual, returns a plainProductionHostApplicationFactory<TEntryPoint>. The doc says to override it only when the host needs a fixture beyond a Production-pinned boot (:18-21).Host_StopsGracefully_FiringLifetimeEventsWithinTimeout(:33-62): the single[Fact]. It creates the factory (:36) and holds the disposal as a separateConfiguredAsyncDisposable(:40), with an inline comment explaining why (:38-39): the shorterawait using var factory = ....ConfigureAwait(false)form would retypefactoryand lose access toCreateClientandStartedHost.- It then creates and immediately disposes a client (
:42-45), which is what forces the lazy host to build and start, assertsStartedHostis non-null (:47-48), resolvesIHostApplicationLifetime(:49), and confirmsApplicationStartedalready fired (:50). - The stop itself (
:55-56): aCancellationTokenSourceforShutdownTimeoutSecondsandawait host.StopAsync(timeout.Token). Reaching the next line already means the stop returned cleanly inside the budget, because a wedged hosted service makes the token cancel andStopAsyncthrow (:52-54). - The two closing assertions (
:58-61):ApplicationStoppingmust have fired during the shutdown, andApplicationStoppedmust signal once the host has fully stopped, each with its own because-reason.
- Why it's built this way: the bounded token is the mechanism, not decoration. Without it a host that
never drains would hang the test run instead of failing it, which is exactly the production symptom the
test exists to surface. Driving a real
IHost.StopAsync(rather than asserting registrations) is what makes this a runtime check. - Where it's used: both gateways, each a body-less one-line subclass,
MMCA.Store/Tests/Hosts/MMCA.Store.Gateway.Tests/GracefulShutdownTests.cs:9andMMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/GracefulShutdownTests.cs:9. Both run in the no-database host-test tier, so they are in each repo'sCI.slnf.
Comparison
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:61· Level 2 · class (private sealed, nested)
- What it is: the private worker nested inside OpenApiBreakingChangeDetector. One instance holds the two parsed roots (baseline and live), the collected findings, and the set of schema pairs already visited, and walks the baseline looking for what the live document dropped or tightened
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:61). - Depends on:
System.Text.Json(JsonElement), OpenApiBreakingChange andOpenApiBreakingChangeKind, plus two private records,OperationSite(:497) andParameterSite(:511). - Concept introduced, a baseline-driven structural diff.
[Rubric §9, API & Contract Design]: the walk iterates the BASELINE and looks each thing up in the live document, so additions are never even visited. That one-sided direction is what makes "additive changes pass" a property of the algorithm, not a filter. - Walkthrough
- State:
OperationNames(:63, the eight HTTP methods),_visited(:65), a privateDirectionenum ofRequest/Response(:67-71), and theChangesset (:73). ComparePaths(:75): per baseline path,RemovedPathif gone; per baseline operation,RemovedOperationif gone, otherwise it builds anOperationSiteand runs the three comparisons below.CompareParameters(:260): parameters are keyed by location and name, name case-insensitive, viaEffectiveParameters(:224), where an operation's own parameters override its path item's. EmitsRemovedParameter,ParameterNewlyRequiredandAddedRequiredParameter;CompareParameterSchema(:301) addsChangedParameterType. A path parameter is always required (ParameterSite.IsRequired,:514).CompareRequestBody(:326):RemovedRequestBody,AddedRequiredRequestBody(only when the live body carriesrequired: true),RemovedRequestMediaType, then the content is compared in the request direction.CompareResponses(:381):RemovedResponseStatusCode, otherwise content compared in the response direction.CompareContent(:403) thenCompareSchema(:423).CompareSchema(:423): resolves$refs, skips a pair already in_visited(which also stops recursive schemas,:433-437), reportsChangedPropertyTypewhen the declared type sets differ (DeclaredTypes,:133, drops thenulltype first,:152, so nullability is not a type change), then recurses throughpropertiesanditems.CompareProperties(:455): a baseline property missing from live isRemovedResponsePropertyonly in the response direction; in the request direction a property newly inrequiredisRequestPropertyNewlyRequired(:484-493).- Helpers:
Escape(:116, RFC 6901~0/~1),TryGetObject(:119),Resolve(:169, follows local#/refs with a 32-hop cap,:171),TryNavigate(:189).
- State:
- Why it's built this way: direction matters. A caller breaks when a request property becomes required; a reader breaks when a response property disappears; a type change breaks both. Pointers move with
$refresolution so a finding names the component, not the reference site. - Where it's used: instantiated once per
Detectcall (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:52). - Caveats / not-in-source: only local
#/references are followed; external refs are left unresolved and the pair is compared as written.
CrossServiceHostIsolation
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:30· Level 2 · static class
- What it is: a static extension on
IServiceCollectionthat narrows one in-process test host to its own modules. Its single entry point,IsolateCrossServiceHost<TEntryPoint>()(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceHostIsolation.cs:42), replaces the host'sIEntityConfigurationAssemblyProviderwith a filtering wrapper so its registry, outbox, internal-command and cleanup processors only enumerate that host's data sources. - Depends on:
IEntityConfigurationAssemblyProvider,DefaultEntityConfigurationAssemblyProviderandEntityConfigurationOptions(itsAdditionalAssembliesare kept regardless of the filter), plus the host's.deps.json. It is the isolation half of CrossServiceFixtureBase, whose class doc names it as the call every host factory makes fromConfigureTestServices(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:43). - Concept introduced, host isolation by dependency manifest.
[Rubric §14, Testability]: when several hosts share one test process, the default AppDomain scan hands each host its peers' module assemblies and, through the per-moduleDataSources__{X}keys, its peers' databases, so each host's outbox would drain them all (CrossServiceFixtureBase.cs:40-44). The runtime closure of a host is exactly what its.deps.jsonlists, so filtering the scanned assemblies to that closure reproduces production's one-host-one-module-set shape (ADR-006). The method is declared in anextension(IServiceCollection services)block, the public DI-surface style of ADR-106. - Walkthrough
IsolateCrossServiceHost<TEntryPoint>(:42-62): reads the host's assembly names (ReadHostAssemblyNames), removes every non-keyedIEntityConfigurationAssemblyProviderdescriptor (:47-53), remembers the last one as the inner provider, and registers a singletonHostScopedAssemblyProviderwrapping it (:56-59); returns the collection for chaining.CreateInner(:65-73): rebuilds the removed registration by shape (none registered givesDefaultEntityConfigurationAssemblyProviderviaActivatorUtilities, then instance, factory or implementation type), throwingInvalidOperationExceptionfor any other shape.ReadHostAssemblyNames(:79-114): takes the host assembly name plus the file name of everyruntimeentry under everytargetslibrary in the.deps.jsonbeside the assembly (:93-111); throwsInvalidOperationExceptionwhen the file is missing (:86-91).HostScopedAssemblyProvider(:116-125): keeps an inner assembly only if it is in the explicitAdditionalAssembliesset or its simple name is in the host's name set.
- Why it's built this way: the manifest is the authoritative closure and needs no per-host allow-list,
so a new module reference flows in automatically. Reading it requires the file beside the assembly, which
WebApplicationFactorycopies for every referenced host project (:89-90). - Where it's used: called from
CrossServiceFixtureBase(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:43) and covered byMMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Fixtures/CrossServiceHostIsolationTests.cs.
SqlServerIntegrationTestFixtureBase<TEntryPoint>
MMCA.Common.Testing ·
MMCA.Common.Testing.Fixtures·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27· Level 2 · class (abstract)
- What it is: the reusable fixture that boots a real service host in-process against a throwaway SQL Server database, applies the module's migrations on first start, resets data between tests with Respawn, and drops the database on disposal. It is the concrete engine behind IIntegrationTestFixture for SQL Server hosts.
- Depends on: IIntegrationTestFixture (implemented,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:27), CrossServiceFixtureBase (its staticComposeConnectionStringhelper,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:88), plusMicrosoft.AspNetCore.Mvc.Testing(WebApplicationFactory),Microsoft.Data.SqlClient,Respawn, andXunit'sIAsyncLifetime(:1-4). - Concept introduced, the disposable-database integration fixture and environment-variable overrides.
[Rubric §14, Testability]and[Rubric §8, Data Architecture]: real integration coverage needs a real relational database, and this fixture makes that cheap and hermetic, a fresh GUID-named database per fixture, migrated from scratch, Respawned between tests, dropped at the end. The database-per-service routing (ADR-006) is why the class doc stresses theDataSourcescollapse onto a single overridden connection string (:16-24). - Walkthrough
- State (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/SqlServerIntegrationTestFixtureBase.cs:30-45): the recorded original-environment map, the server-base and database-name strings, theWebApplicationFactory, theRespawner, a_databaseCreatedflag, and the publicClient/ConnectionString.ConnectionString(:45) is exposed so SQL-fidelity tests can read raw tables (for example to assert an integration event landed in the outbox). Services(:52): the booted host's root service provider, exposed so cross-service tests can resolve a consumer-side integration-event handler or a repository and drive the flow directly against the real database.- Abstract knobs:
SqlBaseEnvironmentVariable(:58, names the env var holding the CI SQL base connection string),DatabaseNamePrefix(:61), andCreateFactory()(:134, where the subclass builds the host).CreateClient()(:64) satisfies the interface by delegating to the factory. InitializeAsync(:67-101): resolves the server base fromSqlBaseEnvironmentVariableor falls back to LocalDB (:69-70), composes a GUID-suffixed database name (:71) and builds the connection string throughCrossServiceFixtureBase.ComposeConnectionString(:72), which overlays the catalog on the base string with aSqlConnectionStringBuilderand forcesTrustServerCertificate(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Fixtures/CrossServiceFixtureBase.cs:88-94), forcesASPNETCORE_ENVIRONMENT=Testingand pushes the top-level SQL connection string as environment variables (:75-76), lets the subclass push its own viaConfigureTestEnvironment(:77), and builds the factory (:79). It then sets_databaseCreated = true(:85) before creating the client, because the flag means "may exist", not "exists": the drop is guarded byIF DB_ID(...), so dropping a never-created database is a no-op, while a host that fails after its migration created the database still has it dropped instead of leaking it on the shared server (:81-85). Creating the client is what triggers the host'sMigrateinit to create the database and apply the module's migrations (:87-89). It then builds theRespawner, ignoring__EFMigrationsHistory(:91-100).ResetDatabaseAsync(:104-117): returns immediately when no respawner exists, otherwise opens a connection and callsRespawner.ResetAsync.DisposeAsync(:120-136): disposes client and factory, drops the database when the flag is set, and restores the environment.ConfigureTestEnvironment(:147-149) is an emptyvirtualhook receiving the setter delegate.SetEnvironmentVariable(:151-160) records only the first original value per key so re-pushing a key cannot clobber the restore point;RestoreEnvironment(:162-170) puts them all back and clears the map.DropDatabaseAsync(:172-193): clears pooled connections so the database is free to drop (:175), connects tomasterthrough the sameComposeConnectionStringhelper (:177), and runs anIF DB_ID(...) IS NOT NULLguardedSET SINGLE_USER WITH ROLLBACK IMMEDIATEplusDROP DATABASE(:185-188), with a scopedCA2100suppression justified because the database name is a server-generated GUID, never user input (:184).
- State (
- Why it's built this way: overrides go through process environment variables because the host reads
its connection string at configure-time; forcing the
Testingenvironment skipsappsettings.Development.json(which would pointDataSourcesatlocalhost) so the resolver collapses onto the single overridden top-level connection string, making the fixture behave like a clean single-database monolith. LocalDB-by-default keeps local runs zero-config while CI can point at a SQL service container. Note the contrast with CrossServiceFixtureBase, which needs the opposite (named sources per host so several EF models can coexist in one process); the two fixtures nevertheless share one pure, static connection-string composer, so the catalog overlay is identical in both and unit-testable without a container (CrossServiceFixtureBase.cs:80-88). - Where it's used: the base of every per-service integration fixture in both apps, four in ADC
(
MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Infrastructure/ConferenceIntegrationTestFixture.cs:18,.../MMCA.ADC.Engagement.IntegrationTests/Infrastructure/EngagementIntegrationTestFixture.cs:18,.../MMCA.ADC.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:23,.../MMCA.ADC.Notification.IntegrationTests/Infrastructure/NotificationIntegrationTestFixture.cs:18) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Infrastructure/CatalogIntegrationTestFixture.cs:16,.../MMCA.Store.Identity.IntegrationTests/Infrastructure/IdentityIntegrationTestFixture.cs:16,.../MMCA.Store.Sales.IntegrationTests/Infrastructure/SalesIntegrationTestFixture.cs:17). Each is then theTFixturefor that service's integration and contract tests. - Caveats / not-in-source: the fixture needs a reachable SQL Server, so these suites build but do not
run without one; they execute in each repo's SQL-service CI job via the
*.Integration.slnffilter.
ProblemDetailsContractTestsBase<TFixture>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22· Level 2 · class (abstract)
- What it is: a contract conformance base that asserts a host's error responses are RFC 9457 Problem
Details documents, machine-readable bodies carrying
status,title, and a diagnostic extension, across both error-shaping paths the framework uses. - Depends on: IntegrationTestBase<TFixture> (inherited,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:22),System.Net,System.Net.Http.Json,System.Text.Json,AwesomeAssertions, andXunit(:1-5). Same live-guard shape as the OpenAPI base above. - Concept: still
[Rubric §9, API & Contract Design], here the pinned contract is the error shape. The class covers the two distinct paths that produce errors (class doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:11-19): ASP.NET Core model validation (a 400application/problem+jsonbody) and the framework'sHandleFailureResult-error mapping (see ApiControllerBase), which turns a Result failure such as an Error not-found into a 404 problem (ADR-013 defines that edge contract). - Walkthrough
Validation_400_HasProblemDetailsShape(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ProblemDetailsContractTestsBase.cs:30-40): sends the subclass's validation probe, asserts the shared shape at 400, then checks theproblem+jsoncontent type and the model-validation-only extensionstype,traceId, anderrors(:35-38).NotFound_404_HasProblemDetailsShape(:41-47): sends the 404 probe and asserts the shared shape.- Abstract probes:
SendValidationErrorProbeAsync(:54) andSendNotFoundProbeAsync(:60), the only app-specific pieces, authenticating first when the endpoint requires it (the docs suggestpageNumber=0against a[Range(1, int.MaxValue)]paged read, and reading an id that does not exist,:49-60). AssertProblemDetailsShapeAsync(:67-83): the sharedprotected staticassertion, JSON content type, echoedstatus, non-emptytitle, and at least one diagnostic extension (errors,traceId, orrequestId,:76-80), returning the parsed body so a subclass can follow up.
- Why it's built this way: pinning both the validation path and the
HandleFailurepath in one base means a regression in either error channel breaks CI, and factoring the shape assertion into a shared static keeps every host's error contract identical while still letting a host with a reachable 409-conflict path layer its own test on top (:16-18). - Where it's used: subclassed per host, four in ADC
(
MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:21,.../MMCA.ADC.Engagement.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17,.../MMCA.ADC.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:17,.../MMCA.ADC.Notification.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ProblemDetailsContractTests.cs:20,.../MMCA.Store.Identity.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16,.../MMCA.Store.Sales.IntegrationTests/Contract/ProblemDetailsContractTests.cs:16). ADC Conference is the one that adds a 409 stale-RowVersionconflict test on top of the inherited facts (.../MMCA.ADC.Conference.IntegrationTests/Contract/ProblemDetailsContractTests.cs:40-67, reusingAssertProblemDetailsShapeAsyncat:67).
ServiceInfoVersioningContractTestsBase<TFixture>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20· Level 2 · class (abstract)
- What it is: a contract conformance base that proves the API-versioning machinery actually works
across more than one version: that
/ServiceInfois served by both v1.0 (deprecated) and v2.0, selected by theapi-versionheader, and that the host reports supported and deprecated versions in response headers. - Depends on: IntegrationTestBase<TFixture> (inherited,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:20),System.Net,System.Text.Json,AwesomeAssertions, andXunit(:1-4). - Concept:
[Rubric §9, API & Contract Design]again, the versioning axis (ADR-046). The class doc (:8-17) makes the point that without a second working version the whole versioning story would be untestable, so this base keeps the machinery exercised rather than merely asserted. Because theServiceInfocontroller ships inMMCA.Common.API(ServiceInfoControllerBase), the entire test body is identical across repos; a subclass supplies only its fixture. - Walkthrough
ServiceInfo_V1_ReturnsMinimalShape_AndIsReportedDeprecated(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/ServiceInfoVersioningContractTestsBase.cs:28-42): requests v1.0, asserts 200, checksapiVersion == "1.0"and that the evolvedsupportedVersionslist is absent in the v1 shape (:35-36), then asserts anapi-deprecated-versionsresponse header contains1.0(:38-40).ServiceInfo_V2_ReturnsEvolvedShape_AndIsReportedSupported(:43-57): requests v2.0, asserts 200, checksapiVersion == "2.0"and thatsupportedVersionscontains2.0(:50-52), then asserts anapi-supported-versionsheader advertises2.0(:54-56).GetServiceInfoAsync(string apiVersion)(:59-65): clears auth and sends the GET with theapi-versionheader set to the requested version, which is the header-based selection ADR-046 standardizes.
- Why it's built this way: keeping a real deprecated v1 and a real v2 side by side, and asserting
both the payload shapes and the
ReportApiVersionsheaders, is what proves version negotiation is wired end to end rather than configured and forgotten. - Where it's used: two adopters today, each a body-less one-line subclass,
MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/ApiVersioningTests.cs:15andMMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/ApiVersioningTests.cs:16. - Caveats / not-in-source: adoption is per-repo partial, one host each in ADC and Store, not every extracted REST service.
OpenApiBreakingChangeDetector
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:38· Level 3 · static class
- What it is: a pure function over two OpenAPI JSON strings.
Detect(baselineJson, liveJson)returns only the changes that break an existing client (removed path, operation, status code, parameter, request body or media type; newly required property, parameter or body; removed response property; changed type) and passes everything else (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeDetector.cs:44-58). - Depends on:
System.Text.JsonandSystem.Globalization(:1-2), the private nested Comparison worker, and the OpenApiBreakingChange record with itsOpenApiBreakingChangeKindenum (thirteen members,MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiBreakingChangeKind.cs:4-44). - Concept introduced, structural rather than textual contract comparison.
[Rubric §9, API & Contract Design]: both documents are read by member name, so property order never shows up as a change, and only the contract-bearing members are read (paths, parameters, in, name, responses, requestBody, content, schema, $ref, type, properties, required, items), so descriptions, summaries, examples and tags are ignored by construction (:14-22). This is the machinery behind the baseline opt-in described in ADR-058. - Walkthrough
Detect(:44-58): guards both arguments withArgumentException.ThrowIfNullOrWhiteSpace(:46-47), parses withJsonDocument(:49-50), runsComparison.ComparePaths(:52-53), and returns the findings ordered by pointer (ordinal) then kind (:55-57), so the report is deterministic.- Everything else lives in Comparison (
:61-519).
- Why it's built this way: a static, dependency-free function is trivially unit-testable against hand-written documents, which is how the framework tests it (
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/OpenApiBreakingChangeDetectorTests.csandOpenApiBreakingChangeDetectorParameterTests.cs); the contract base only supplies the two strings. - Where it's used: called by
OpenApiContractTestsBase<TFixture>.OpenApiDocument_HasNoBreakingChangesAgainstTheBaseline(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:149) and see OpenApiContractTestsBase<TFixture>.
TenantResolutionTestsBase
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27· Level 3 · class (abstract)
- What it is: a settings-level conformance base for multi-tenant hosts. A subclass supplies the host's
TenancySettingsand inherits three facts that must stay true: header resolution is an explicit opt-in, the signed claim wins over the header, and the health probes bypass tenant resolution (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/TenantResolutionTestsBase.cs:27-101). - Depends on: TenancySettings and TenantResolutionStrategy (the order and prefixes read through
EffectiveResolutionOrderandEffectiveExcludedPathPrefixes),PathStringfor segment-prefix matching, and AwesomeAssertions. It does not boot a host, so it needs no fixture. - Concept introduced, tenant isolation as a configuration invariant.
[Rubric §14, Testability]: a header-resolved tenant lets any caller name any tenant, so the dangerous configuration is caught as a failing test on the settings object rather than discovered in production (ADR-073, ADR-058). - Walkthrough
- Knobs:
ProbePaths(:30,/healthand/alive), abstractSettings(:33), virtualAllowsHeaderResolution(:40, defaultfalse). Header_resolution_is_an_explicit_opt_in(:47): passes when tenancy is disabled or the host opted in; otherwiseEffectiveResolutionOrdermust not containTenantResolutionStrategy.Header, and the failure message tells the owner what to change.Claim_is_tried_before_header(:67): when the order includes the header, the claim must appear and come before it.Health_endpoints_are_excluded(:88): each probe path must be covered by an effective excluded prefix, matched withStartsWithSegmentsand case-insensitively as the middleware does, because a probe rejected for carrying no tenant takes the replica out of rotation.
- Knobs:
- Why it's built this way: the opt-in override exists for a host behind a trusted gateway that sets the header itself; making that a deliberate
overridekeeps the exception visible in code review. - Where it's used: exercised in the framework by
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/TenantResolutionTestsBaseTests.csandTenantResolutionConformanceTests.cs.
OpenApiContractTestsBase<TFixture>
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32· Level 2 · class (abstract)
- What it is: a contract conformance base that boots a host and asserts its
/openapi/v1.jsondocument is served, is a well-formed OpenAPI 3.x document, and still describes the core public resources, so an accidental controller or route removal fails CI instead of silently changing the published contract. - Depends on: IntegrationTestBase<TFixture> (inherited,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:32),System.Net,System.Text.Json,AwesomeAssertions, andXunit(:1-4), plus OpenApiBreakingChangeDetector for the opt-in baseline test. - Concept introduced, the contract guard on the live document.
[Rubric §9, API & Contract Design]assesses whether the API surface is described and kept stable; the default of all three Level 2 bases is a live-document guard with no committed snapshot (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/OpenApiContractTestsBase.cs:15-17), the assertions run against the document the host actually serves, so new controllers can never leave a stale snapshot behind and a removed one is caught immediately. A subclass can additionally opt in to a committed baseline diff that fails only on breaking changes. This is one of the suites recorded in ADR-058, and the one with the widest adoption. - Walkthrough
- Overridable and abstract knobs:
OpenApiDocumentPath(:51, defaults to/openapi/v1.json),MinimumPathCount(:58, a coarse floor under the route surface),MinimumPathCountBecause(:65, the failure-message reason),CorePublicResources(:71, the resource paths that must keep being described), andBaselineDocumentPath(:80,nullby default, which turns the baseline test into a no-op). RefreshBaselineEnvironmentVariable(:41,"MMCA_UPDATE_OPENAPI_BASELINE"): the public constant that switches the baseline test into refresh mode; the privateIndentedOptions(:43) indents the rewritten file.OpenApiDocument_IsServed_AsWellFormedOpenApiDescribingTheApiSurface(:83-95): parses the JSON and assertsopenapistarts with3.,info.titleis non-empty, apathsobject exists, and it holds at leastMinimumPathCountentries.OpenApiDocument_DescribesEveryCorePublicResource(:98-115): first guards against a vacuous pass (the subclass must pin at least one resource,:100-101), then checks everyCorePublicResourcesentry is present, matching on name case-insensitively (:107-110) so presence, not exact casing, is the contract.OpenApiDocument_HasNoBreakingChangesAgainstTheBaseline(:118-159): returns immediately whenBaselineDocumentPathis null (:120-124). With the variable set to1it rewrites the baseline from the live document (indented,:131-132) and then fails on purpose (:135-140), so a regenerating run can never pass in CI. Otherwise it asserts the baseline file exists (:143), runsOpenApiBreakingChangeDetector.Detect(:149) and requires zero changes, listing each one plus theRefreshGuidancetext (:176).GetOpenApiJsonAsync(:165-174): clears auth (the document is anonymous outside Production), fetches the path, asserts 200 with a message naming the path, and returns the raw JSON.Indent(:180) re-serializes the document for a line-by-line diffable baseline.
- Overridable and abstract knobs:
- Why it's built this way: asserting the live document by default keeps the guard maintenance-free while still catching the two failures that matter, the document disappearing and a public resource vanishing; the two assertions are deliberately coarse because a strict shape diff would fail on every ordinary additive change. Hosts that want a stricter contract opt in to the baseline, whose detector ignores additive and documentation-only changes, so the gate fires only on what breaks a client.
- Where it's used: subclassed once per REST service host, four in ADC
(
MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Contract/OpenApiContractTests.cs:16,.../MMCA.ADC.Engagement.IntegrationTests/Contract/OpenApiContractTests.cs:15,.../MMCA.ADC.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:16,.../MMCA.ADC.Notification.IntegrationTests/Contract/OpenApiContractTests.cs:17) and three in Store (MMCA.Store/Tests/Integration/MMCA.Store.Catalog.IntegrationTests/Contract/OpenApiContractTests.cs:15,.../MMCA.Store.Identity.IntegrationTests/Contract/OpenApiContractTests.cs:15,.../MMCA.Store.Sales.IntegrationTests/Contract/OpenApiContractTests.cs:15), each supplying the fixture, the path floor, and the pinned resource list.
InMemoryRefreshSessionStore
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:27· Level 6 · sealed class
- What it is: an in-memory
IRefreshSessionStore fake for handler and application-service
unit tests, giving a test both a seedable list of already-persisted sessions and a staged-insert buffer
that only lands on the seeded list once
SaveChangesAsyncruns, so a test can assert what a handler wrote without a database. - Depends on: IRefreshSessionStore (implemented,
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:27) and RefreshSession (:29-30), the identifier aliasUserIdentifierType, andSystem.ThreadingforCancellationToken. - Concept introduced, the stage-then-flush fake. Rather than mutate a single backing list on every
call, the fake keeps two lists:
_staged(:29) for rows an in-flight unit of work has added but not yet saved, and_sessions(:30) for what a real store would already have durably persisted.AddAsync(:63-67) only appends to_staged; onlySaveChangesAsync(:155-162) moves the staged rows onto_sessionsand incrementsSaveCount(:157). This mirrors the two-phase add-then-SaveChangesAsynccontract every IUnitOfWork-backed repository has, so a test exercising a handler that forgets to callSaveChangesAsyncsees the session absent fromSessions, the same gap a real database would show.[Rubric §14, Testability]is the fit: no database, no host, and the fake still models the commit boundary rather than eagerly persisting on every write. - Walkthrough
- Backing state (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/InMemoryRefreshSessionStore.cs:29-30): the private_stagedand_sessionslists.Sessions(:33) exposes_sessionsread-only, in insertion order.SaveCount(:36) counts how many times the store was flushed. RotationOutcome(:43): an optionalFunc<bool>a test sets to forceTryRotateAsyncto report a lost race, standing in for the database arbitrating two concurrent rotations of the same token where the loser writes nothing (doc:39-41).- Seeding helpers:
Seed(RefreshSession)(:47) drops an already-built session straight onto_sessions, bypassing the staged path;SeedLive(userId, token, createdAt, expiresAt)(:55-60) builds one viaRefreshSession.Create(...).Value!and seeds it, for a test that only has raw field values rather than a session in hand. AddAsync(:63-67): stages the session;FindByTokenHashAsync(:70-71) does an ordinal-equality scan over_sessions(never_staged) by token hash;GetUnrevokedByUserAsync(:74-87) filters_sessionsby user id and!IsRevoked, ordering byCreatedAtthenIdto match the real store's contract;FindByIdAsync(:139-144) scans_sessionsby id and user id together;FindByIdUntrackedAsync(:151) scans_sessionsby id alone. The real store uses it to read past a tracked copy, but this fake has no tracked copy to see past (every read hands out the one persisted instance, which already reflects any rotation), so the "fresh" read returns the same instance.- Session-administration reads, all over
_sessionsonly:GetAllByUserAsync(:90-103) returns every session for a user including revoked ones, ordered byCreatedAtthenId(the unfiltered counterpart ofGetUnrevokedByUserAsync);GetUserIdsWithLiveSessionsAsync(:106-119) returns the distinct owner ids of sessions whereIsActiveAt(now)holds;CountLiveSessionsByUserAsync(:122-136) null-guardsuserIds, then groups the active sessions of the requested users into a per-user count (users with no live session are absent from the dictionary). SaveChangesAsync(:155-162): incrementsSaveCount, moves every staged row onto_sessions, clears_staged, and returns the count written.TryRotateAsync(:165-188): null-guards both sessions (:171-172); consultsRotationOutcometo simulate a lost race (:174-177); callspresented.Revoke(revokedAt, RefreshSession.ReasonRotated, successor.TokenHash)and bails out on failure (:179-182); on success, stages the successor and immediately flushes viaAddAsyncplusSaveChangesAsync(:184-185), so a caller sees the successor inSessionswithout a separate save call.
- Backing state (
- Why it's built this way: the staged/persisted split is what lets a test distinguish "the handler
called
AddAsync" from "the handler's unit of work actually committed," which is exactly the bug class the framework'sSaveChangesAsync-returns-1-by-default convenience (HandlerTestBase<THandler>) is meant to catch for other aggregates.FindByTokenHashAsynconly ever sees_sessions, never_staged, so an uncommitted add is invisible to a lookup, matching how a real store would behave mid-transaction. - Where it's used: seeded and asserted against directly in
MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/AuthenticationServiceTests.cs(3 references) and across MMCA.Common suites,MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/RefreshSessionRevocationTests.cs(3),MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Users/DeleteUserHandlerBaseTests.cs(3),MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceBaseTests.cs(2),MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/RefreshSessionManagementTests.cs(2),MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/Administration/UserSessionsAdministrationServiceTests.cs(1),MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceIdentityCompletionsTests.cs(1), andMMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServiceLegalAcceptanceTests.cs(1).
MiddlewarePipelineOrderTestsBase
MMCA.Common.Testing ·
MMCA.Common.Testing.Conformance·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:31· Level 11 · class (abstract)
- What it is: the HTTP-edge counterpart of DecoratorPipelineOrderTestsBase<TCommand, TCommandResult, TQuery, TQueryResult>. It seeds the framework's default middleware step list, applies the host's own customization if it has one, and asserts the resulting step order is exactly the documented pipeline, plus that the startup-validated adjacency invariants still hold.
- Depends on:
MiddlewarePipelineBuilder and
MiddlewarePipelineStepNames from
MMCA.Common.API.Startup.Pipeline(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:2), plusAwesomeAssertionsandXunit(:1,3). This is the reference that makesMMCA.Common.Testingdepend onMMCA.Common.API(MMCA.Common/Source/Hosting/MMCA.Common.Testing/MMCA.Common.Testing.csproj:51-53). - Concept introduced, order-as-data at the HTTP edge. In ASP.NET Core middleware order is behavior,
not style, and the failures it produces do not look like ordering bugs. The class doc pins four
adjacencies and names two runtime failures they guard against
(
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:13-17): a tenant that never resolves when tenant resolution runs before authentication, and a per-user rate cap that never engages when the limiter runs before authentication. It is explicit that the third adjacency, the pre-forwarded capture immediately beforeUseForwardedHeaders, has no runtime consequence today: nothing in the framework reads the captured Items, and the OIDC discovery endpoint derivesjwks_urifromJwt:Issuer, so that adjacency is pinned as structure only (:17-19). What makes the check cheap is that ADR-079 turned the order into data:MiddlewarePipelineBuilder.CreateDefault()produces a list of named steps that are inert until applied, so noWebApplicationhas to be built and the test runs in the fast unit tier with no database and no host (:27-28).[Rubric §17, DevOps & Deployment]assesses whether cross-cutting edge concerns are composed deliberately;[Rubric §11, Security]is why authentication before the rate limiter is load-bearing (ADR-019);[Rubric §14, Testability]and[Rubric §34, Architecture Governance & Documentation]cover the fitness-function form itself (ADR-015). - Walkthrough
Configure(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:37):virtual, defaults tonull, meaning the host under test calls the zero-argumentUseCommonMiddlewarePipeline()overload. A host that customizes the pipeline overrides this with the sameAction<MiddlewarePipelineBuilder>itsProgram.cspasses (:22-24).ExpectedStepNames(:39-60): the pinned order, outermost first, all eighteen steps named throughMiddlewarePipelineStepNamesconstants rather than string literals: ExceptionHandler, CorrelationId, RequestLocalization, PreForwardedCapture, ForwardedHeaders, HttpsRedirection, ResponseCompression, Routing, Cors, Authentication, TenantResolution, RateLimiting, SoftDeletedUserFilter, Authorization, OutputCache, JwksEndpoint, OidcDiscoveryEndpoint, Controllers. It isvirtual, so a host with a deliberately different pipeline states its own order.EdgePipeline_OrdersSteps_InDocumentedOrder(:62-69): the first[Fact]. It builds the seeded builder and assertsbuilder.StepNamesequalsExpectedStepNames, with a because-reason (:68) that spells out three adjacencies rather than just reporting a list mismatch: the pre-forwarded capture immediately beforeUseForwardedHeadersso the captured pre-forwarded scheme and host stay faithful, tenant resolution immediately after authentication so the claim strategy seesHttpContext.User, and the rate limiter after authentication per ADR-019 so the per-user cap engages.EdgePipeline_SatisfiesLoadBearingInvariants(:71-79): the second[Fact]. It assertsbuilder.Build()does not throw.Build()(MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Pipeline/MiddlewarePipelineBuilder.cs:251-274) re-checks four invariants at startup, PreForwardedCapture immediately before ForwardedHeaders (:253-256), Authentication immediately before TenantResolution (:258-261), Authentication before RateLimiting (:263-266), and ForwardedHeaders before HttpsRedirection (:268-271), so a pipeline that fails here would have thrown while the host was starting.CreateBuilder(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Conformance/MiddlewarePipelineOrderTestsBase.cs:81-86): the two-line private helper both facts share,MiddlewarePipelineBuilder.CreateDefault()followed byConfigure?.Invoke(builder).
- Why it's built this way: the two facts are complementary rather than redundant. The first pins the
exact order, so any reorder (including one that still satisfies every adjacency) fails visibly; the
second re-runs the host's own startup validation in the unit tier, so an override that breaks an
adjacency fails in a test rather than at boot. Naming steps through
MiddlewarePipelineStepNamesconstants means a step rename is a compile error in the test rather than a silent string mismatch. - Where it's used: four subclasses, every one of them body-less because every host calls the
zero-argument overload,
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Conformance/MiddlewarePipelineOrderTests.cs:12,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/MiddlewarePipelineOrderTests.cs:16, andMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15. The framework's ownUseCommonMiddlewarePipelinedoc points back at this base as the way to freeze the order (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:45).
HandlerTestBase<THandler>
MMCA.Common.Testing ·
MMCA.Common.Testing.Support·MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:38· Level 14 · class (abstract)
- What it is: the reusable Moq scaffold for command/query handler unit tests. It hands a derived
test class a pre-configured
Mock<IUnitOfWork>, a no-op logger typed to the handler under test, and two one-line helpers that register a repository mock into that unit of work. - Depends on: IUnitOfWork,
IRepository<TEntity, TIdentifierType>,
IReadRepository<TEntity, TIdentifierType>,
AuditableAggregateRootEntity<TIdentifierType>
and
AuditableBaseEntity<TIdentifierType>
(the two generic constraints), plus
Moq,Microsoft.Extensions.Logging, andNullLogger<T>(MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:1-5). - Concept: the arrange-phase base class. Where
IntegrationTestBase<TFixture> gives an end-to-end test a booted host,
this gives an isolated unit test a mocked persistence boundary: no database, no host, no HTTP. The
class doc (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:10-12) frames it as the shared replacement for the per-test copy-paste ofMock<IUnitOfWork>plusGetRepositorywiring plusSaveChangesAsyncsetup.[Rubric §14, Testability]assesses whether the design permits fast isolated tests; the fact that handlers depend onIUnitOfWork(an Application-layer abstraction) rather than aDbContextis what makes this scaffold possible at all, which is[Rubric §3, Clean Architecture]paying off in the test tier (ADR-055 records that contract).[Rubric §15, Best Practices & Code Quality]covers the deduplication itself. - Walkthrough
- Constructor (
MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:41-42): a single expression-bodied statement that pre-configuresUnitOfWork.SaveChangesAsync(...)to return1, the success path, so a happy-path test writes no persistence setup at all. Failure-path tests override it with their ownSetup(doc:32-35). UnitOfWork(:45): theMock<IUnitOfWork>every registered repository is wired into, created by a property initializer so the constructor can configure it.Logger(:48):NullLogger<THandler>.Instance, typed by the handler type parameter so it binds directly to the handler'sILogger<THandler>constructor parameter.RegisterRepository<TEntity, TIdentifierType>()(:56-64): creates aMock<IRepository<TEntity, TIdentifierType>>, wires that same object into bothGetRepository<...>()andGetReadRepository<...>()(:61-62), and returns the mock for furtherSetup/Verify. Constrained toAuditableAggregateRootEntity<TIdentifierType>(:57), that is, to aggregate roots.RegisterReadRepository<TEntity, TIdentifierType>()(:72-79): the read-only counterpart for non-aggregate child entities that expose no read-write repository, constrained to the looserAuditableBaseEntity<TIdentifierType>(:73) and wiring onlyGetReadRepository<...>()(:77).
- Constructor (
- Why it's built this way: wiring one repository mock into both accessors matters because a handler
may read through
GetReadRepositoryand write throughGetRepositoryon the same aggregate; a test forced to register two mocks would have to keep their state in sync. Pre-succeedingSaveChangesAsyncencodes the common case so only the interesting deviation appears in a test. - Where it's used: 123 test files today, the framework's own scaffold test
(
MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13) plus 122 ADC Application-tier files spread across all four modules (for exampleMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Categories/UseCases/CreateConferenceCategoryHandlerTests.cs:13,MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/UseCases/CastVoteHandlerTests.cs:13,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ChangePassword/ChangePasswordHandlerTests.cs:14,MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/UserNotificationExportServiceTests.cs). The class doc carries a workedCreateEventHandlerTestsexample (MMCA.Common/Source/Hosting/MMCA.Common.Testing/Support/HandlerTestBase.cs:19-31). - Caveats / not-in-source: adoption is uneven. MMCA.Store and MMCA.Helpdesk handler tests do not
subclass it at all: a workspace-wide search finds no
HandlerTestBasereference under either repo'sTests/tree. Why those two arrange their handler tests by hand is not recorded in source.
AnonymousEndpointTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:30· Level 0 · abstract class
- What it is: an allow-list gate for authorization opt-outs. Every
[AllowAnonymous]in the assemblies a subclass names must appear in an explicit, reviewed list, so an endpoint cannot quietly lose its authorization gate. A second gate, on by default and opt-out per repo, closes the complementary hole: an endpoint carrying no authorization attribute at all. - Depends on:
[Fact](xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and reflection over attribute instances matched by full name:AllowAnonymousAttribute,AuthorizeAttribute,ControllerBase, and Blazor'sRouteAttributeare four private full-name constants (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/AnonymousEndpointTestsBase.cs:32-35), not compile-time references. - Concept introduced, the two-directional allow-list gate. A one-directional "no anonymous endpoints" rule is unusable (login has to be anonymous) and a one-directional "these are allowed" rule rots. This base asserts both halves: no unlisted
[AllowAnonymous]exists, and no list entry matches nothing. The second half is the subtle one, since a stale entry hides a renamed or re-gated endpoint behind a permission that is no longer being granted (reason string, line 114).[Rubric §11, Security]assesses whether authentication gates stay where they were put;[Rubric §14, Testability]assesses turning a review-only property into an executable one;[Rubric §34, Architecture Governance]applies because the exception list, with its per-entry justification comments, becomes the reviewed record of the anonymous surface. - Walkthrough
- The subclass supplies
TargetAssemblies(line 38),AllowedAnonymousEndpoints(line 45, identified as the type'sFullNamefor a type-level attribute andFullName.MethodNamefor a method-level one, lines 41-43), and aMinimumScannedTypesfloor (line 52, default 1). - Three allow-list
[Fact]s.AnonymousEndpoints_AreAllowListed(line 80) subtracts the allow-list from the discovered set and names what is left.ScannedEndpointSet_IsNotEmpty(line 92) is the non-vacuity guard: with no controllers and no routable components discovered, the first assertion passes without having looked at anything (comment, lines 94-95).AllowList_HasNoStaleEntries(line 105) runs the comparison the other way. - Two shapes are scanned, both by reflection:
IsController(line 159) walks the base chain forControllerBase, andIsRoutableComponent(line 175) looks forRouteAttributeon the type, combined inIsScannedEndpointType(line 153). AnonymousEndpoints()(line 183) isprotectedrather than private so a subclass can build a richer report over the same data (doc, lines 178-182); it flattens the assemblies, filters to the scanned shapes, and returns a distinct, ordinally-ordered set.AnonymousEndpointsOf(line 255) is the load-bearing detail: type-level attributes are read withinherit: false(line 257) and methods are enumeratedDeclaredOnly(line 264), so a framework base action is reported once at its declaration site instead of once per derived controller in every consumer repo (comment, lines 262-263).- The undecorated-endpoint gate is enforced by default:
RequireExplicitAuthorizationDecision(line 69) returnstrue, and its companion listEndpointsWithoutAuthorizationAttribute(line 77) defaults to empty.Endpoints_DeclareAnAuthorizationDecision(line 119) fails any undecorated endpoint not in that companion list (lines 127-132). A repo that is not ready overrides the flag tofalse, and the fact then returns immediately without checking anything (lines 121-125); the earlier fallback that asserted the two scans stay disjoint is gone.UndecoratedAllowList_HasNoStaleEntries(line 137) is that list's own stale-entry guard, the same shape asAllowList_HasNoStaleEntries, and runs regardless of the flag. UndecoratedEndpoints()(line 199) mirrorsAnonymousEndpoints()but selects types and actions with no decision at all.UndecoratedEndpointsOf(line 207) skips abstract types on purpose, since the concrete controller inheriting the action set is where the decision belongs, and reports per type rather than per action, because a controller that forgot[Authorize]usually declares no method of its own (comment, lines 209-213).DeclaresAuthorizationAnywhere(line 229) walks the base-type chain checking both the type and its declared methods for either attribute;HasAuthorizationAttribute(line 250) is the sharedAuthorize-or-AllowAnonymoustest both members reduce to.
- The subclass supplies
- Why it's built this way: the class doc is explicit about the limit (lines 18-24). Minimal-API endpoints opt out through the
.AllowAnonymous()builder call, which produces endpoint metadata at map time and is invisible to static reflection, so the framework's own minimal-API anonymous surface (JWKS, OIDC discovery, app-association, session-cookie refresh, health) is outside this gate; catching it would need an endpoint-metadata check over a built host. Matching ASP.NET types by full name keeps the package free of an ASP.NET reference, the same stance the whole rule library takes (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29). The stricter gate defaults on (line 69; doc lines 54-60): the allow-list check alone cannot see an endpoint carrying no authorization attribute at all, exactly the shape a forgotten[Authorize]produces, so a repo must decline it explicitly by overriding the flag tofalserather than adopt it by remembering to. On a controller ASP.NET Core's fallback authorization policy now closes that hole at run time, butAuthorizeRouteViewgates a routable Blazor component by reading attributes and ignoring the fallback policy entirely, so for pages this test IS the control (remarks, lines 61-68). - Where it's used: subclassed in all four repos, and the
DeclaredOnlydecision is what keeps each list local to what that repo declares. MMCA.Common's AnonymousEndpointTests lists the credential-exchange actions on its controller bases (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTests.cs:14, list at:22, floor of 21 at:43). ADC's covers the two Identity credential actions plus the public conference-browse reads, calendar exports and bookmark counts, with a floor of 79 (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/AnonymousEndpointTests.cs:23, floor at:108). Store's covers the public storefront reads, product images, registration and the Stripe webhook, with a floor of 32 (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/AnonymousEndpointTests.cs:10, floor at:54). Helpdesk's single entry is its wholeTicketsController, because the seed ships without an Identity issuer so there is no token to require (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/AnonymousEndpointTests.cs:17, entry and reason at:26-30, floor of 1 at:35). MMCA.Common also carries the adversarial coverage for the base itself in AnonymousEndpointTestsBaseTests, which proves each assertion fails on its own drift through privateDriftedTests,StaleAllowListTestsandEmptyScanTestssubclasses (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/AnonymousEndpointTestsBaseTests.cs:163,:108,:117) plus a conformant control (:126).
ArchitectureAssert
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:8· Level 0 · static class
- What it is: the shared failure-reporting helper for every architecture fitness function, a static class with two
NoViolationsoverloads that turn a rule breach into a readable, offender-listing assertion failure. - Depends on:
NetArchTest.Rules.TestResultand AwesomeAssertions'Should()fluent API, both global-imported for the whole package (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/GlobalUsings.cs:3-4). No first-party dependencies: this is the bottom of the fitness-function stack. - Concept introduced, architecture fitness functions. A fitness function is an automated test that asserts a structural property of the codebase (a layer never references another, a controller is sealed) rather than a behavioral one. This package makes those rules first-class, shared code (ADR-015).
[Rubric §14, Testability]assesses how well invariants are guarded by executable checks;ArchitectureAssertis the reporting primitive that makes a failing invariant name its offenders instead of just going red.[Rubric §34, Architecture Governance]assesses whether architectural decisions are enforced rather than merely documented; every rule in this package funnels its verdict through here. - Walkthrough
NoViolations(NetArchTest.Rules.TestResult result, string reason)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureAssert.cs:11) returns early whenresult.IsSuccessful(line 13), otherwise null-coalescesFailingTypesto an empty list, joins the full names into a bullet list (lines 18-19), and assertsIsSuccessful.Should().BeTrue(...)with the reason plus the violation list as thebecauseargument (lines 21-22).NoViolations(IEnumerable<string> violations, string reason)(line 26) materializes the sequence once and assertslist.Should().BeEmpty(...)(line 30), for the reflection-derived, IL-reading and file-scanning rules that produce a plain string list rather than a NetArchTest result.
- Why it's built this way: the XML doc (lines 3-7) names it the un-drifted successor to the three per-repo
ArchitectureTestHelper.AssertNoViolationscopies: the reporting logic was duplicated in MMCA.Common, MMCA.Store, and MMCA.ADC, and centralizing it here removes the drift. - Where it's used: every rule in ArchitectureRules and several reflection-based test bases call one of these two overloads as their final step.
BrandColorTokenTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:13· Level 0 · abstract class
- What it is: an abstract xUnit test base that fails the build when a landing-page stylesheet re-hardcodes the brand hex instead of sourcing it from the shared
var(--mmca-primary)CSS custom property. - Depends on:
[Fact](xUnit), AwesomeAssertions, andAssembly.GetManifestResourceStream(BCL) to read embedded CSS. No first-party type dependency: it operates on the strings the subclass embeds. - Concept introduced, the drift fitness function. Unlike a layer rule that reflects over assemblies, a drift function reads committed text (CSS here) and asserts a single source of truth is used.
[Rubric §20, Design System & Theming]assesses whether visual tokens have one authoritative definition; this base guards that consumers of the framework palette cannot silently fork the primary color. - Walkthrough
- Two private constants pin the forbidden literal
#1565C0and the required tokenvar(--mmca-primary)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/BrandColorTokenTestsBase.cs:15-16). - The subclass supplies
EmbeddedCssLogicalNames(line 22), the manifest-resource names of its landing-page stylesheets. - The single
[Fact]LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex(line 25) first asserts the list is non-empty (a non-vacuity guard, lines 27-28), then for each stylesheet reads it viaReadEmbeddedCss(line 56, which throws a clearInvalidOperationExceptionwhen the resource is missing, lines 58-60) and records a violation when the file is blank (line 37), when the token is absent (line 43), or when the raw hex is present (line 48, matched withOrdinalIgnoreCaseso a lowercase spelling cannot slip past). - Resources are resolved from
GetType().Assembly(line 33), that is, the subclass's assembly, which is what lets a package-shipped base read a consumer's stylesheet.
- Two private constants pin the forbidden literal
- Why it's built this way: the doc (lines 3-12) explains the split. MMCA.Common's own BrandColorTokenTests guards the C#-to-CSS token definition (from
BrandColors.Primary), while this base guards every downstream consumer of it, embedding the stylesheets as manifest resources so the package needs no file-system access into the consumer repo. - Where it's used: subclassed once per repo that ships a branded landing page, as
BrandColorTokenTestsin ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/BrandColorTokenTests.cs:12) and Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/BrandColorTokenTests.cs:10).
CallGraphIndex
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:20· Level 0 · internal sealed class
- What it is: a read-only index over the compiled assemblies an IL-reading rule scans, built once per rule run. It holds every type by full name, every type's transitive ancestor names, and the reverse map from an ancestor name to the scanned types that implement or derive from it.
- Depends on:
Mono.Cecil(ModuleDefinition,TypeDefinition,TypeReference,MethodDefinition,MethodReference), which the package already carries transitively through NetArchTest, plus BCL dictionaries andHashSet<string>. No first-party dependencies. - Concept introduced, following an interface call in a static walk. IL records the static callee: a handler that calls
IPointsAwarder.AwardAsyncemits acallvirtnaming the interface method, not the class that does the work. A walk that stops there never reaches the implementation, so a transitive rule would report nothing. The reverse implementor map is what closes that gap (doc, lines 9-13). The second design decision is equally load-bearing: ancestors are recorded by name, not by resolved definition, so a contract declared in an assembly the map does not register (typically a framework interface such asIDomainEventHandler<T>) is still recognized as an ancestor; only the walk through such a type stops, because its metadata is absent (doc, lines 14-18).[Rubric §14, Testability]assesses whether behavioral invariants inside method bodies can be checked at all; this index is the primitive that makes the body-reading rules in this package possible without a Roslyn or IL-rewriting dependency. - Walkthrough
- Three ordinal-keyed dictionaries back the index:
_typesby full name,_ancestorsper type, and_implementorsfrom an ancestor name to the derived types (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/CallGraphIndex.cs:22-24). - The constructor (line 26) makes two passes: it registers every type in every module, nested types included (lines 28-31), then computes each type's ancestor set and inverts it into
_implementors(lines 33-48). Types(line 52) exposes the whole inventory.ElementFullName(line 55) strips a generic instantiation down to the open definition's name, which is how a reference to a closed generic still matches the type it was declared as.Implements(type, ancestorFullName)(line 58) is the membership test the handler and cascade rules select on.Find(reference)(line 63) resolves a reference to a scanned type or null when it lives outside the scan.TargetsOf(callee)(line 72) is the core: it resolves the declaring type, collects the directly matching methods, and returns them unchanged when the declaring type is neither an interface nor virtual (lines 80-84); otherwise it adds every matching method on every known implementor (lines 86-87). Overload resolution is approximated by name and parameter count, which deliberately over-approximates so no real path is missed (doc, lines 66-71).MatchingMethods(line 94) requires a body and an equal parameter count, and accepts either an exact name match or a.-suffix match, because an explicit interface implementation is namedSome.Ns.IFoo.Methodin metadata (doc, lines 90-93).ComputeAncestors(line 116) walks base types and interfaces with an explicit stack, adding each name once and descending only into definitions the scan actually contains (lines 122-138), which is both cycle-safe and tolerant of out-of-scan contracts.
- Three ordinal-keyed dictionaries back the index:
- Why it's built this way: the package's whole stance is zero compile references to the code it governs (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29), and reading Cecil metadata by name is the same discipline applied one level deeper. Building the index once per rule run, rather than resolving references per call site, is what keeps a transitive breadth-first walk over a whole repo's assemblies fast enough to sit in a normal test run. - Where it's used: by the three IL-walking rules that need to follow calls rather than just find them:
AggregatesCascadeSoftDeleteToChildren(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:115) andDomainEventHandlersDoNotSave(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:93), surfaced through CascadeSoftDeleteConventionTestsBase and DomainEventHandlerSaveTestsBase. - Caveats / not-in-source: the type is
internal, so a consumer repo cannot build its own call-graph rule on top of it; the reach is only through the public rules.
CostTagConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:29· Level 0 · abstract partial class
- What it is: a cost-attribution gate over a consumer's IaC template. It parses the embedded
infra/main.bicepand asserts that everyMicrosoft.App/containerAppsresource carries aservice:key on itstags:line, and that each per-service database carries the sameservicedimension, so one cost report splits compute and storage per service. - Depends on:
[Fact](xUnit), AwesomeAssertions, a source-generated[GeneratedRegex], andAssembly.GetManifestResourceStream. No first-party dependency and no IArchitectureMap: like ObservabilityConventionTestsBase it reads embedded text, not assemblies. - Concept introduced, the tagging-convention gate.
[Rubric §17, DevOps]assesses whether the IaC template is the checked source of truth, and[Rubric §34, Architecture Governance]whether a convention is enforced rather than described; the failure closed here is the silent one, a new container app or database added without the tag, so its spend lands in an unattributed bucket. The deployed-cost model itself is the subject of the cost guide, not this base. - Walkthrough
- Two abstract knobs:
MinimumContainerApps(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/CostTagConventionTestsBase.cs:36), the non-vacuity floor, andDatabaseNamePrefix(line 42), the prefix stripped from a database name to derive its tag (for exampleADC_). Two virtual ones:BicepResource(line 45, defaultinfra.main.bicep) andResourceAssembly(line 51, defaulting toGetType().Assembly). EveryContainerApp_CarriesAServiceTag(line 59) runs theContainerAppDeclarationregex over the bicep, asserts at leastMinimumContainerAppsmatches (line 64, so a drifted anchor fails instead of passing over nothing), then lists every match whosetagsgroup lacksservice:(line 69, an ordinalContains). The check reads the key on that one line only, not the union shape: by convention the line is a union ofcommonTagswith aservicekey, but anytags:line that namesservice:passes (doc comment, lines 54-56).ServiceDatabases_CarryTheServiceTagOfTheOwningApp(line 83) requires a non-blank prefix, then asserts the bicep contains the exact linetags: union(commonTags, { service: toLower(replace(dbName, '<prefix>', '')) })(lines 88-93). The database check, unlike the container-app one, does pin the union shape.ReadEmbedded(line 96) throws anInvalidOperationExceptionnaming the assembly when the resource is not embedded.- The
[GeneratedRegex]partial propertyContainerAppDeclaration(lines 107-108, 2000 ms timeout) matches a container-app resource header and up to three body lines before itstags:line; a resource with notags:line in that window still matches with an empty group, so it is reported as untagged rather than skipped (comment, lines 104-106).
- Two abstract knobs:
- Why it's built this way: the
ResourceAssemblydefault is the same load-bearing choice as the other embedded-text bases: the base ships inside the framework package, so resolving against its own assembly would look for the consumer's bicep in the wrong DLL. A subclass needs only theEmbeddedResourceentry for its template. - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/CostTagConventionTests.cs), and exercised adversarially in MMCA.Common byMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/CostTagConventionTestsBaseTests.cs. Line numbers for those two files: Not determinable from this unit's source excerpt.
CrossEntityNavigationFinder
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97· Level 0 · private sealed class
- What it is: a private
ExpressionVisitornested in ArchitectureRules that walks a specification'sCriterialambda and collects the names of other entity types it navigates into. - Depends on:
System.Linq.Expressions.ExpressionVisitor,MemberExpression,PropertyInfo(BCL) and the RuleHelpers extension propertyInheritsAuditableEntity. - Concept introduced, expression-tree inspection as a fitness check. NetArchTest reasons about assembly-level references only; to catch a rule expressed inside a lambda body, the code instantiates the specification, reads its
Criteriaexpression tree, and visits it. This backs the polyglot and database-per-service invariant (ADR-006): aCriteriathat navigates to an entity in another physical data source produces an untranslatable join at runtime.[Rubric §8, Data Architecture]assesses cross-source data access discipline; this visitor is how that discipline is machine-checked. - Walkthrough
- The primary constructor captures
ownEntityType(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:97), and_navigatedis the accumulatingHashSet<string>(line 99). Find(Expression body)visits the body and returns the set (lines 101-105).VisitMember(line 107) resolves the accessed property's type throughEntityTypeOf(line 121) and, when the result is an auditable entity other than the specification's own type, adds its name (lines 111-115).EntityTypeOf(line 121) treats a direct entity property as a navigation (lines 123-126) and unwraps generic collection navigations such asICollection<TChild>to their element type (lines 129-136).
- The primary constructor captures
- Why it's built this way: filtering by a foreign-key column is engine-portable; navigating is not (notably on Cosmos, where the cross-source relationship is degraded out of the model). The finder is the enforcement half of
ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(line 24), whose doc and failure message point authors at CrossSourceSpecification instead (line 15, lines 74-76). The rule is deliberately best-effort: only parameterless specifications can be instantiated and inspected (lines 37-41), and a specification whose constructor orCriteriathrows on standalone evaluation is skipped rather than failing the suite (lines 49-59). - Where it's used: only inside that rule (line 66), which is surfaced through SpecificationConventionTestsBase.
FlagDeclaration
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FeatureFlags.cs:85· Level 0 · private sealed record
- What it is: one feature-flag constant flattened out of its
[FeatureFlag]attribute, so the two feature-flag rules read as plain data checks (Name,Lifetime,RemoveBy,Owner, all nullable exceptName,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FeatureFlags.cs:85). - Depends on: nothing; a private record nested in ArchitectureRules.
- Concept:
[Rubric §17, DevOps]and ADR-031 govern feature-flag lifecycle: a flag is eitherPermanent(a capability a host chooses) orTemporarywith a parseable ISOyyyy-MM-ddRemoveBy.FeatureFlagsDeclareLifetimereads everypublic const stringon a*Featuresclass, reflects its[FeatureFlagAttribute](matched by full name, line 10, since the attribute itself lives inMMCA.Common.Shared.FeatureFlagsand this package takes no framework reference) throughDescribe(line 95), and checks the resultingFlagDeclarationfor a missing declaration, aPermanentflag carrying a strayRemoveBy, or aTemporaryflag with no parseable one (lines 38-57).TemporaryFeatureFlagsAreNotPastRemoveByfilters the same declarations toTemporaryones whoseRemoveByis in the past (lines 74-78): a failing build here IS the dead-toggle detector, since the branch the flag guards has outlived its rollout. - Where it's used: produced by
Describe(line 95) and consumed by both rules inArchitectureRules.FeatureFlags.cs, surfaced throughFeatureFlagLifecycleTestsBase(line 28, line 70).
ForeignKeyDeleteFact
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DeleteBehavior.cs:88· Level 0 · private sealed record
- What it is: one declared foreign key's delete-behavior facts, read off a finalized EF Core model: a
Descriptionfor failure messages, theDeleteBehaviorname, whether the relationshipIsOwnership, and theMMCA:DeleteBehaviorSourceannotation value or null (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DeleteBehavior.cs:88-92). - Depends on: nothing; a private record nested in ArchitectureRules. Read purely by reflecting over EF's stable public metadata surface (
GetEntityTypes,GetDeclaredForeignKeys,IsOwnership,DeleteBehavior,Properties,FindAnnotation), because the package carries no EF Core reference (doc, lines 97-103). - Concept:
RestrictDeleteByDefaultConventionstamps every foreign key withMMCA:DeleteBehaviorSource = ExplicitorConvention(line 6).CascadingForeignKeysAreExplicitlyOptedIn(line 34) flags a non-ownership relationship that cascades without anExplicitsource, since an unwritten cascade deletes child rows below the aggregate's invariants, the soft-delete filter, and the domain events the framework raises (doc, lines 22-27); ownership foreign keys are exempt because EF requires their cascade rather than offering it (lines 29-31).ConventionForeignKeysRestrictDeletes(line 65) is the complementary half: every relationship left to the convention must restrict, so a model cannot drift onto a third, unchosen behavior.[Rubric §8, Data Architecture]assesses whether destructive data operations stay deliberate; this pair is that discipline made executable. - Where it's used: produced by
ForeignKeyDeleteFacts(line 107) and consumed by both rules inArchitectureRules.DeleteBehavior.cs(lines 38, 69).
Layer
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:9· Level 0 · enum
- What it is: the closed vocabulary of architectural layers a fitness function can reason about:
Shared,Domain,Application,Infrastructure,Api,Ui,Grpc,Contracts,ServiceHost(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:11-19). - Depends on: nothing; a plain enum.
- Concept introduced: the Clean Architecture layer taxonomy made into a type. The layer flow itself is taught in primer §1; here it becomes an enum the rule library keys off, so a rule that iterates layers is written once against the enum rather than hard-coded per repo.
[Rubric §3, Clean Architecture]assesses whether the layering is explicit and enforced; this enum is the shared alphabet. - Walkthrough: the doc (lines 3-8) notes that
Ui,Grpc,Contracts, andServiceHostare optional: a repo simply omits them from its map when absent, so a rule iterating them is vacuously satisfied with no compile dependency on the missing assembly. ArchitectureMapBase.Segmenttranslates each member to its namespace segment, and two of those translations are not the identity mapping:Apibecomes"API"andServiceHostbecomes"Service"(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:105-117). - Where it's used: carried by LayerRef, projected by IArchitectureMap
.OfLayer, and threaded through nearly every method in ArchitectureRules.Contractsis the one member no repo registers today, which is exactly why ServiceContractPurityTestsBase and ContractImplementationTestsBase are attribute-driven rather than layer-driven.
MessageBusBackpressureTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:31· Level 0 · abstract class
- What it is: a broker-backpressure gate over each service's embedded
appsettings.json. For every listed service it asserts theMessageBussection sets bothPrefetchCountandConcurrentMessageLimitto positive values, and that the prefetch window is at least the concurrency it feeds. - Depends on:
[Fact](xUnit), AwesomeAssertions,System.Text.Json(JsonDocument), andAssembly.GetManifestResourceStream. No first-party dependency: it reads settings text, not assemblies. - Concept introduced, the unbounded-default gate. A service whose
MessageBussection omits either knob silently runs on the transport defaults, and a prefetch window below the concurrency starves its consumers;[Rubric §12, Performance & Scalability]is the category the failure message cites.[Rubric §34, Architecture Governance]applies because the bound becomes a checked property of every service's committed configuration instead of a review habit. - Walkthrough
- Three public constants name the configuration:
MessageBusSectionName(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/MessageBusBackpressureTestsBase.cs:34),PrefetchCountKey(line 37) andConcurrentMessageLimitKey(line 40). - The subclass supplies
Services(line 52), the short service names.ResourceAssembly(line 58) defaults to the derived type's assembly andAppSettingsResource(service)(line 63) defaults toservices.{service}.appsettings.json. EveryServiceAppSettings_BoundsBrokerBackpressure(line 70) assertsServicesis not empty (the non-vacuity guard), runsCheckover each service's embedded settings, and fails with the joined violation list.Check(service, appSettingsJson)(line 93) ispublic staticso the parse can be exercised over a fixture document directly. It parses with trailing commas and comments tolerated, returns a single "declares no MessageBus section" violation when the section is absent or not an object (lines 99-103), then reports each missing or non-positive knob (lines 109, 114) and a prefetch below the concurrency (line 119).ReadInt(line 129) accepts a JSON number or a quoted number, because the configuration binder reads every value as a string (comment, line 141).TryGetProperty(line 151) matches names case-insensitively, mirroring configuration binding.ReadEmbedded(line 166) throws a message naming the assembly when the resource is missing.
- Three public constants name the configuration:
- Why it's built this way: parsing the JSON directly, rather than binding it, keeps the package free of the framework's options types, the same stance the rest of the rule library takes. Exposing
Checkpublicly means the gate's own logic is tested with fixture strings instead of embedded files. - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/MessageBusBackpressureTests.cs) and covered in MMCA.Common byMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Deployment/MessageBusBackpressureTestsBaseTests.cs. Line numbers for those two files: Not determinable from this unit's source excerpt.
ModuleConformanceTestsBase<TModule>
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:21· Level 0 · abstract generic class
- What it is: a per-module conformance gate. It asserts that one module's
Name,Dependencies, andRequiresDependenciesstill say what the repo expects, and gives the subclass a hook to assert whatRegisterDisabledStubsputs in the container. - Depends on:
[Fact](xUnit), AwesomeAssertions, andSystem.Reflection(GetInterfaces,GetProperty,BindingFlags). It has noMap: it reflects over a single module type, not over an assembly inventory. The contract it reads is IModule, matched by the full-name constant"MMCA.Common.Application.Modules.IModule"(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleConformanceTestsBase.cs:24). - Concept introduced, gating a silent-drift contract. The three members this base checks are the whole surface ModuleLoader registers on: it resolves the topological (Kahn) registration order from
Dependencies, matchesModulesSettingsentries byName, and decides between a hard start failure and stub registration fromRequiresDependencies(doc, lines 4-10). Drift in any of the three throws nothing: it silently reorders registration, leaves a module permanently enabled, or swaps a real service for a disabled stub. That is exactly the failure class a fitness function exists for.[Rubric §7, Microservices Readiness]assesses whether module boundaries and their declared dependencies stay honest as modules move between hosts;[Rubric §14, Testability]assesses whether a silent contract is made loud;[Rubric §34, Architecture Governance]applies because the module contract is enforced rather than described. The module system itself is taught in Group 14. - Walkthrough
- The type parameter is constrained
where TModule : class, new()(line 22), so the defaultCreateModule()is justnew()(line 68); a module without a parameterless constructor overrides that hook. - Three expectation members the subclass declares: the abstract
ExpectedName(line 27), and the virtualExpectedDependencies(line 30, defaulting to the empty list for a leaf module) andExpectedRequiresDependencies(line 36, defaulting tofalse). - Four
[Fact]s.Module_ShouldDeclare_ExpectedName(line 39) compares the readName, with abecausespelling out the consequence: renaming silently disables the module's configuration and drops it from other modules' dependency graphs (line 42).Module_ShouldDeclare_ExpectedDependencies(line 45) casts the value toIEnumerable<string>, asserts it is not null (the shapeModuleLoadersorts on, lines 49-50), thenBeEquivalentTothe expectation (lines 52-54).Module_ShouldDeclare_ExpectedRequiresDependencies(line 58) pins the flag that turns a disabled dependency into a startup failure instead of a substituted stub (line 61).Module_ShouldRegister_ExpectedDisabledStubs(line 64) delegates to theAssertDisabledStubshook. AssertDisabledStubs(line 77) is deliberately empty by default: a module exporting no cross-module contract registers no stubs, so the fact passes vacuously and only a module that does export one overrides it (doc, lines 70-76).- The private
ReadContractMember(line 81) is the load-bearing mechanism. It finds theIModuleinterface on the instance by full-name match (lines 85-87), asserts the module implements it at all (lines 89-90), resolves the named public instance property off the interface (line 92), and reads it (line 99). Reading through the interface property dispatches to the module's override when there is one and to the framework's default interface implementation when there is not (comment, lines 97-98).
- The type parameter is constrained
- Why it's built this way: the reflection-by-full-name approach is the same discipline the rest of the package uses, and for the same reason: it keeps the package free of the framework's transitive graph (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:22-29). It also buys a second property the doc calls out (lines 11-18): dispatching through the interface property is what asserts a leaf module againstIModule's defaults, the reach the hand-written per-repo tests needed an explicit(IModule)cast for. - Where it's used: subclassed once per module across the consumer repos, five times today: ADC's
NotificationModuleTests(MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8, the fullest example: it expects["Identity"],RequiresDependenciestrue, and overridesAssertDisabledStubsto assert theIUserNotificationExportServicestub descriptor is a singleton) andIdentityModuleTests(MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/IdentityModuleTests.cs:5), plus Store's Catalog, Sales, and Identity module tests (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.API.Tests/CatalogModuleTests.cs:5,MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/SalesModuleTests.cs:8,MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.API.Tests/IdentityModuleTests.cs:5). MMCA.Common holds the adversarial coverage for the base itself in ModuleConformanceTestsBaseTests, which runs it against a leaf and a dependent fake module (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ModuleConformanceTestsBaseTests.cs:51,:60) and proves each assertion actually fails on the drift it claims to catch through a privateDriftedTestssubclass (:131).
ObservabilityConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:30· Level 0 · abstract partial class
- What it is: an SLO alert-to-runbook pairing gate. It parses the SLO metric alerts a consumer's
infra/main.bicepprovisions and asserts each one keeps a matching, severity-correct triage section in that repo'sinfra/OPERATIONS.md, in both directions: a missing runbook section fails, and so does an orphan runbook section whose alert no longer exists. A fourth check, on by default, additionally requires the bicep to provision an operator-facing monitoring view (an Azure Monitor workbook or a portal dashboard) next to those alerts. - Depends on:
System.Globalization, source-generatedSystem.Text.RegularExpressionsregexes,Assembly.GetManifestResourceStream, AwesomeAssertions, and[Fact](MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/ObservabilityConventionTestsBase.cs:1-2). No first-party dependency and, notably, no IArchitectureMap: it reads embedded text, not assemblies. - Concept introduced, the documentation-pairing gate.
[Rubric §13, Observability & Operability]assesses whether an operator paged at 3am has usable guidance; the failure mode this base closes is the silent one, an alert added, renamed, or re-tiered while its runbook section stays behind.[Rubric §17, DevOps]applies because the source of truth is the IaC template itself, and[Rubric §34, Architecture Governance]because it makes the "every alert has a runbook" convention executable rather than aspirational. - Walkthrough
- Overridable knobs:
MinimumAlertSpecs(line 39, default 3), the two manifest-resource logical namesBicepResource(line 42,infra.main.bicep) andRunbookResource(line 45,infra.OPERATIONS.md), andResourceAssembly(line 51), which defaults toGetType().Assembly. A fifth knob,RequireWorkbook(line 60), defaults totrue: an alert an operator cannot look at the signal behind is half a monitoring story, so every consumer gets the check without opting in. A subclass overrides it tofalseonly for a deployment that deliberately has no operator-facing view, and says why in the override. MonitoringWorkbookOrDashboard_IsProvisioned_WhenRequired(lines 67-79) returns immediately whenRequireWorkbookis off; otherwise it assertsMonitoringViewResourceRegexfinds at least oneresourcedeclaration of typeMicrosoft.Insights/workbooksorMicrosoft.Portal/dashboardsin the bicep (line 77), so the signals the alerts fire on also have a place an operator can look at them.SloAlertSpecs_AreDiscovered_GateIsNotVacuous(lines 81-89) is the honesty guard: discovering fewer specs than the floor means the parse anchors drifted, not that alerts disappeared.EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection(lines 91-117) matches each alert key against the runbook headings throughHeadingNamesAlert(line 101, built on the-alert-infix constant at line 32) and then asserts the heading also carries the(sev N)tag matching the bicep severity (lines 108-112).EveryRunbookAlertSection_MapsToAProvisionedAlert(lines 119-131) is the reverse direction, flagging stale guidance, and reads headings through the same predicate (line 126).HeadingNamesAlert(lines 156-178,protected static) is the one shared pairing predicate. It matches-alert-{key}as a whole token: an optional-v{digits}version suffix is allowed, then the heading must end, or continue with whitespace,(or a closing backtick (regex at line 175, 2 second timeout). Any other hyphenated continuation is a different alert, so the section forserver-response-time-p99cannot answer forserver-response-time, and an orphanx-extrasection cannot hide behind a survivingxalert, as a plain substring test would have allowed.DiscoverAlertSpecs(lines 133-154) slices the bicep betweenvar sloAlertSpecsandresource sloAlerts(lines 137-140, each index assertion carrying its ownbecause), runsAlertKeyRegexandAlertSeverityRegexover that block, and asserts the two match counts agree (line 145) so a changed spec shape fails loudly instead of silently mis-pairing.DiscoverRunbookAlertHeadings(line 180) keeps only the###headings carrying the infix. The four[GeneratedRegex]partial properties sit at lines 191-201, each with a 2000 ms match timeout; the last,MonitoringViewResourceRegex(lines 200-201), is multiline and case-insensitive.ReadEmbedded(lines 182-189) throws a message naming the assembly when a resource is missing.
- Overridable knobs:
- Why it's built this way: the class doc (lines 23-28) records why the
ResourceAssemblydefault is load-bearing. The base ships inside the framework package, so resolving resources against its own assembly would look for the consumer's bicep insideMMCA.Common.Testing.Architecture.dlland always throw. Defaulting to the derived type's assembly means a subclass needs no wiring beyond twoEmbeddedResourceentries in its csproj (the snippet is in the doc at lines 18-22). - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), where the single override raisesMinimumAlertSpecsfrom the base default of three to six (:14) so the gate cannot pass vacuously on a drifted parse, and in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/ObservabilityConventionTests.cs:7), whose one override raises the floor to five (:13); see ObservabilityConventionTests. Neither overridesRequireWorkbook, so both inherit the defaulttrueand their bicep must provision a workbook or dashboard. MMCA.Common subclasses it twice. ObservabilityConventionTestsBaseTests is a deliberate cross-assembly guard that repoints both resource names at local fixtures (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/ObservabilityConventionTestsBaseTests.cs:14, resources at:16-18) and adds one extra[Fact]pinning theResourceAssemblydefault to the derived type's assembly rather than the base's (:24-29). SampleDeploymentObservabilityTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SampleDeploymentObservabilityTests.cs:12) runs the gate over the framework's ownsamples/deploymentbicep and runbook (:14-16) with a floor of four (:20), and restatesRequireWorkbook => trueexplicitly (:24), since the sample is the deployment consumers copy.
PiiInventoryRow
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:79· Level 0 · private sealed record
- What it is: one parsed row of the personal-data inventory table in a repo's
PRIVACY.md:Memberis theType.Propertythe row names (backticks trimmed) andCellsholds every cell keyed by its column header, compared case-insensitively (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:79). - Depends on: nothing beyond
IReadOnlyDictionary<string, string>; a private record nested in the ArchitectureRules partial. - Concept: the privacy gate keeps the published policy and the code in step, and it needs the policy side as data.
ParsePiiInventory(line 103) walks the Markdown line by line, accepts a table only when its header holds theMember,Purpose,Lawful basisandRetentioncolumns, skips the separator row, and builds onePiiInventoryRowper data row (line 135).[Rubric §11, Security]and the privacy criteria assess whether declared personal data matches what the system actually marks; the record is what the orphan check compares against the live[Pii]members. - Walkthrough:
PiiInventoryRowsMapToPiiMembers(line 61) takes theType.Propertykeys of every[Pii]property in the supplied assemblies (PiiMemberKeys, line 85), then flags each parsed row whoseMemberis not among them (lines 66-69), failing throughArchitectureAssert.NoViolations(line 71) with the instruction to remove the row or restore the marker in the same change. - Where it's used: private to
ArchitectureRules.PiiInventory.cs; no usage outside the defining file.
ProtoScopeKind
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:393· Level 0 · private enum
- What it is: the five block kinds the
.protoparser tracks while walking a file:Service,Message,Enum,Oneof, andBlock, the catch-all for any other braced block such as an rpc option body or an aggregate option value (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:393-402,Blockat line 401). - Depends on: nothing; a private enum nested in ArchitectureRules.
- Concept: the proto contract gate parses
.protofiles with a hand-rolled line scanner, not a protobuf compiler, so it needs an explicit notion of "which block am I inside" to decide how to render the current line, and to know which closing brace pops which scope. The kind is whatDescribeMemberbranches on (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:283): aBlockscope renders nothing, since it holds options only and nothing on the wire (lines 285-289), aServicescope renders rpcs (lines 293-304), anEnumscope renders values (lines 306-312), and anything else renders message fields (from line 314).[Rubric §9, API & Contract Design]assesses whether a published contract surface is pinned deliberately; this enum is the vocabulary that pinning is expressed in. - Walkthrough:
TryPushScope(line 255) maps the regex-captured keyword to a member (lines 263-269), withOneofas the discard fallback arm.Oneofexists specifically to be treated as transparent: aoneofblock contributes no name segment because on the wire its members belong to the enclosing message (comment plus push, lines 271-273).Blockis never produced byTryPushScope: the statement loop pushes an anonymousBlockscope itself when a statement that is not a scope header ends with{(lines 161-166), so the block's closing brace pops that scope rather than the enclosing message or service. - Where it's used: carried by ProtoScope (line 407) and consumed by
DescribeMember(line 283); both are private to theArchitectureRules.Protos.cspartial.
RouteAuthorizationTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:22· Level 0 · abstract class
- What it is: an abstract test base that reflects over a UI assembly's routable Blazor pages and fails the build if a page the subclass marks as governed has lost its
[Authorize(Roles = "...")]role gate. - Depends on:
[Fact](xUnit), AwesomeAssertions, RuleHelpers.LoadableTypes, and pure reflection over attribute instances matched by full name (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/RouteAuthorizationTestsBase.cs:24-25). - Concept introduced, the security-regression fitness function.
[Rubric §11, Security]and[Rubric §25, Navigation & IA]assess whether protected routes stay protected; this base turns "the admin page must require the Organizer role" from a review checklist into a compiled assertion, so a page cannot silently regress from a role-scoped[Authorize]to a bare[Authorize]reachable by any authenticated user. It is the page-level counterpart to AnonymousEndpointTestsBase, which guards the opt-out side of the same question. - Walkthrough
- The subclass supplies
TargetAssembly(line 28), the exactRequiredRole(line 31), anIsGovernedPagestrategy (line 40), and aMinimumGovernedPagesnon-vacuity floor (line 47, default 1). GovernedPages_RequireDeclaredRole(line 50) collects pages that are routable, governed, and do not require the role, then asserts the offender set is empty, naming each offender's route templates (lines 52-60).GovernedPageSet_IsNotEmpty(line 64) guards the guard: if a refactor moved namespaces soIsGovernedPagematched nothing, the first test would pass vacuously, so this one asserts the discovered count meets the floor (lines 68-73).- Detection is all reflection by attribute full name:
IsRoutablePage(line 77),RequiresRole(line 86, which reads theRolesproperty off everyAuthorizeAttributeinstance, own and inherited, and passes when any one requires the exact role by ordinal match, so a bare[Authorize]or a different role alone fails; the attribute allows multiple instances and ASP.NET Core enforces all of them, so a policy-only sibling or a derived page's bare[Authorize]cannot hide the role gate, and the result no longer depends on which instance reflection returns first), the subclass helperHasAuthorizeAttribute(line 97),Routes(line 100), and the base-type walkIsOrDerivesFrom(line 108) that lets a derived authorize attribute still count.
- The subclass supplies
- Why it's built this way: matching attributes by full name keeps the shared package free of an ASP.NET Core reference (lines 16-20) while still inspecting ASP.NET attributes; the package's only dependencies are NetArchTest, AwesomeAssertions, and xUnit (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/MMCA.Common.Testing.Architecture.csproj:18-20). A reflection scan also covers future pages matching the strategy without hand-enumeration. Deliberately anonymous public pages and bare-[Authorize]self-service pages simply must not matchIsGovernedPage(lines 12-15). - Where it's used: subclassed per module UI test project, five times today:
MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/ManagementRouteAuthorizationTests.cs:19,MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:16, and Store's Catalog, Sales, and Identity equivalents (MMCA.Store/Tests/Modules/Catalog/MMCA.Store.Catalog.UI.Tests/CatalogRouteAuthorizationTests.cs:15,MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/SalesRouteAuthorizationTests.cs:18,MMCA.Store/Tests/Modules/Identity/MMCA.Store.Identity.UI.Tests/IdentityRouteAuthorizationTests.cs:15).
RuleHelpers
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:14· Level 0 · internal static class
- What it is: the internal reflection toolbox the reflection-based fitness functions share: extension members for enumerating loadable types, matching suffix conventions on generic types, detecting base types and interfaces by open generic or name prefix, and classifying property setters as mutable or
init-only. - Depends on:
System.Reflection(Assembly,Type,PropertyInfo,ReflectionTypeLoadException,BindingFlags) only. - Concept introduced: the doc (lines 5-9) states the premise: NetArchTest cannot inspect method return types, generic-argument constraints, property accessors, or attribute usage, so those rules reflect over loaded types directly through these helpers.
[Rubric §14, Testability]and[Rubric §15, Best Practices & Code Quality]apply: the reflection subtleties (partial assembly loads,init-only detection) are solved once here rather than re-derived per rule. - Walkthrough: the class body is three C# preview
extension(T)blocks, so every helper is an extension property or method, not a classicthis-parameter extension method (the syntax is taught in primer §4).extension(Assembly assembly)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/RuleHelpers.cs:16):LoadableTypes(line 19) tolerates a partially-resolvable assembly by catchingReflectionTypeLoadExceptionand returning the types that did load throughOfType<Type>(), which both filters nulls and narrows the element type (lines 27-31).ConcreteClasses(line 36) narrows that to non-abstract classes.Classes(line 45) returns every class including abstract ones (interfaces still excluded), for rules whose subject can legitimately be an abstract base a consumer derives from, such as an abstract handler base declared beside the contract it serves.extension(Type type)(line 49):SimpleName(line 56) strips the generic-arity backtick so suffix conventions match generic types too.InheritsGeneric(line 67) walks the base chain andImplementsGeneric(line 81) scans the interface set for an open generic.HasBaseTypeStartingWith(line 89) detects a framework base by full-name prefix without a compile dependency (for example FluentValidation'sAbstractValidator).DeclaredPublicProperties(line 103) narrows to declared-only public instance properties.InheritsAggregateRoot(line 110) andInheritsAuditableEntity(line 117) hard-code the MMCA entity base full names (AuditableAggregateRootEntity<TIdentifierType> plus the AuditableBaseEntity<TIdentifierType> and BaseEntity<TIdentifierType> ancestors) so the entity rules can classify types cross-repo.extension(PropertyInfo property)(line 123):HasPublicMutableSetter(line 130) is the immutability primitive. It reportsfalsewhen there is no public setter, andfalseforinit-only setters by looking for theSystem.Runtime.CompilerServices.IsExternalInitrequired custom modifier on the setter's return parameter (lines 140-144).
- Why it's built this way: every helper avoids a compile-time reference to the type it detects (base types matched by string prefix), which is what lets one rule body run identically across four repos that do not reference each other. The class carries a file-level
[SuppressMessage]for CA1708 (lines 10-13): with multipleextension(T)blocks in one static class the analyzer flags the compiler-generated grouping members as case-colliding, a documented false positive. - Where it's used: throughout the ArchitectureRules partials, inside CrossEntityNavigationFinder, and directly by RouteAuthorizationTestsBase and AnonymousEndpointTestsBase.
- Caveats / not-in-source: the type is
internal, so consumer repos cannot call these helpers directly; they reach the same behavior only through the public rules and bases. StateManagementConventionTestsBase is the one base that re-implements the tolerant type load privately rather than using this class (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:142).
LayerRef
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31· Level 1 · sealed record
- What it is: an immutable record describing one assembly in a repo's architecture: its owning
Module, its Layer, the compiledAssembly, and itsRootNamespace(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:31). - Depends on: Layer and
System.Reflection.Assembly. - Concept introduced: the atomic unit of an architecture map.
Moduleis the empty string for framework (MMCA.Common) layers that belong to no business module (lines 22-30), which is how the same record models both a module assembly and a shared framework assembly. Every projection and every isolation rule keys off that one convention. - Walkthrough: a four-parameter positional
sealed record(line 31), so it gets structural equality and immutability for free; its members are set once at construction by the map'sDefineLayers. - Where it's used: ArchitectureMapBase stores a lazy
IReadOnlyList<LayerRef>and derives every projection from it; itsFrameworkandModulefactory helpers are what build these. The namespace-cycle rule takes aLayerRefdirectly, using itsRootNamespaceto decide which namespace node a type belongs to (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:84); the[ServiceContract]purity rule iteratesmap.Layersdirectly rather than one projection (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:40); and the IL-reading rules resolve each ref's on-diskAssembly.LocationthroughScannableAssemblyLocations(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:88).
ProtoScope
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:407· Level 1 · private sealed record
- What it is: one open block in the
.protoparser's scope stack: a ProtoScopeKind plus the block's name, empty for a transparentoneof(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:404-407). - Depends on: ProtoScopeKind. Private to the
ArchitectureRules.Protos.cspartial. - Concept: the two-field record is what makes nested messages and enums render under their qualified name without a real grammar.
DescribeProtoFilekeeps aStack<ProtoScope>(line 112), pushes on a scope header and pops on a line starting with a closing brace (lines 124-132), andQualifiedName(line 234) reverses the stack and joins the non-empty names under the file'spackage. - Walkthrough: a two-parameter positional
sealed record(line 297), constructed only inTryPushScope(line 184). Because theOneofarm passesstring.Emptyas the name, the length filter inQualifiedName(line 238) drops it, which is exactly the wire semantics: aoneofmember is a field of the enclosing message. - Where it's used:
DescribeProtoFile(line 109),TryPushScope(line 166),DescribeMember(line 194), andQualifiedName(line 234), all feeding ProtoContractTestsBase throughBuildProtoContract(line 86).
SpanishAccentTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:27· Level 1 · abstract class
- What it is: a localization-quality fitness function: no
*.es.resxfile under a repo's resource root may carry a common Spanish word with its accent or n-tilde missing (for example the unaccentedsesionorcontrasenaspellings of the words that need an accent or an n-tilde), so a new Spanish UI string cannot silently regress. - Depends on: ArchitectureAssert (
NoViolations, line 123), ArchitectureMapBase.FindRepoRoot(only in the subclass, to build the root),System.Xml.Linq(XDocument),System.Text.RegularExpressions, AwesomeAssertions. - Concept introduced, a text-quality gate over resource files. Like FormsConventionTestsBase it reads files rather than assemblies, but here the subject is translated UI text. A word list of unaccented spellings (
DefaultUnaccentedWords,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SpanishAccentTestsBase.cs:33-40, 35 words includingsesion,codigo,contrasena,numero,pagina,aqui,mas) is compiled into one case-insensitive whole-word regex with a one-second match timeout (lines 83-87), and everydata/valuepair in every*.es.resxis matched against it. Words that are correct both with and without the accent depending on meaning are deliberately left off the list: the class doc namesesta,soloandaun("even" unaccented, "still" accented) (lines 20-24). - Walkthrough: the subclass supplies one abstract member and may override three virtual ones.
ResourceRoot(line 46) is the folder scanned recursively, normally the repo'sSourcefolder underArchitectureMapBase.FindRepoRoot("<Repo>.slnx").UnaccentedWords(line 52) defaults to the framework list; a subclass extends it with[.. base.UnaccentedWords, "extra"]for domain vocabulary.AllowedEntries(line 59, default empty) lists intentional exceptions as{path relative to the root, forward slashes}:{resource key}, exactly as a failure prints them; the doc comment frames an exception as a decision, not a way to make the gate pass.MinimumResourceFileCount(line 65, default 1) is the anti-vacuity floor: a wrong root that finds no*.es.resxfails instead of passing with nothing checked (lines 79-81).- The single
[Fact]Spanish_resources_keep_their_accents(lines 67-68) asserts the root is set and exists (lines 70-71), enumerates and ordinally sorts the files while skipping build output throughIsBuildOutput(lines 73-77, helper at line 130: any path segmentbin,obj,node_modulesor.git), skips non-string entries that carry atypeormimetypeattribute (lines 99-102) and anyAllowedEntrieshit (lines 107-110), and collectsfile:key: wordsoffenders reported throughArchitectureAssert.NoViolations(lines 118, 123-126).
- Why it's built this way: the offender line is formatted to be pasted straight into
AllowedEntries, so the fix path (correct the value, or record the exception) is in the failure itself. Keeping the word list short and the matcher whole-word keeps false positives rare enough that exceptions stay exceptional. - Where it's used: subclassed with only
ResourceRootoverridden in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/SpanishAccentTests.cs:11, root at:13-14, so the framework's own UI strings are held to the rule every consumer inherits), in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SpanishAccentTests.cs:9, root at:11-12) and in Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/SpanishAccentTests.cs:8, root at:10-11). MMCA.Common also pins the base's own behavior against fixture resources inSpanishAccentFitnessTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Localization/SpanishAccentFitnessTests.cs:12), through a private subclass that pointsResourceRootat a fixture tree (:37-39).
IArchitectureMap
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:39· Level 2 · interface
- What it is: the single per-repo abstraction every architecture fitness function keys off. Each repo supplies one implementation declaring its layer and module assemblies; the shared rule library and abstract test bases consume only this interface, so a rule is written once and runs identically across every repo that supplies a map (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/IArchitectureMap.cs:33-38, whose doc names MMCA.Common, MMCA.Store and MMCA.ADC; MMCA.Helpdesk supplies a fourth map). - Depends on: LayerRef, Layer,
System.Reflection.Assembly. - Concept introduced, the architecture map as the fitness-function extension point. This is a classic Dependency Inversion: the rules depend on an abstraction (the map), and each repo provides the concrete inventory of its assemblies.
[Rubric §1, SOLID](DIP) and[Rubric §7, Microservices Readiness]apply: the map also models the per-module layers a would-be extracted service owns, so the isolation rules can check module boundaries the same way in any repo. - Walkthrough: the interface exposes identity (
RepoTokenline 42,ModuleNamesline 45), the rawLayersinventory (line 48), and the projections the rules lean on:OfLayer(all assemblies of a kind, line 51), the per-moduleModuleDomain/ModuleApplication/ModuleShared(lines 54-60),Infrastructure()/Api()across framework plus modules (lines 63-66), the lookupsFor(module, layer)(line 69) andModuleOf(assembly)(line 72), namespace derivationRootNamespace(module, layer)(line 75), andOtherModuleNamespaces(line 81), which returns the same-layer namespaces of every other module (the forbidden targets for a module-isolation rule, empty for framework layers and single-module repos). - Why it's built this way: funneling every rule through one interface is what removed the drifting per-repo copies of the architecture-test suite (ADR-015); add a repo and you write one map, not a new rule set.
- Where it's used: held as the
protected abstract IArchitectureMap Mapon nearly every*TestsBasein this group and passed to nearly every method of ArchitectureRules. ArchitectureMapBase is the reusable partial implementation, and CommonArchitectureMap / AdcArchitectureMap are two of the four concrete maps (Store and Helpdesk supply the others).
ArchitectureMapBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:11· Level 3 · abstract class
- What it is: the reusable base implementation of IArchitectureMap: a repo supplies only
RepoTokenand aDefineLayers()declaration, and every projection, namespace derivation, and module-isolation target computation is derived here (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureMapBase.cs:3-10). - Depends on: IArchitectureMap, LayerRef, Layer,
System.Lazy, andSystem.IO(forFindRepoRoot). - Concept introduced, the template-method shape for a repo map: the base fixes the algorithm and the subclass fills two holes. It also centralizes every namespace and assembly string in one file, which the doc (lines 8-9) notes fixes Ubuntu CI case-sensitivity in one place.
- Walkthrough
- The constructor wraps
DefineLayers()in aLazy<IReadOnlyList<LayerRef>>(lines 15-16) so the assembly list materializes once, andLayersreads that value (line 25). ModuleNames(line 28) filters out framework refs, then distinct-orders the module names ordinally.OfLayer(line 35),ModuleDomain/ModuleApplication/ModuleShared(lines 39-45, via the privateModuleLayer, line 101),Infrastructure(line 48), andApi(line 51) are one-line LINQ projections overLayers.For(line 54) andModuleOf(line 59) are the lookups, both ordinal-comparison based.RootNamespace(line 63) branches on module: framework layers become theMMCA.Commonprefix plus the segment, module layers the repo token plus the module plus the segment (lines 64-66).OtherModuleNamespaces(line 69) maps every other module through it.- The static
FindRepoRoot(solutionFileName)(line 79) walks up fromAppContext.BaseDirectoryto the directory containing the named.slnx, so doc and config consistency tests can read committed files regardless of the runner's working directory, throwing a clearInvalidOperationExceptionwhen not found (lines 89-90). - The
protected static Framework(...)(line 94) andprotected Module(...)(line 98) factory helpers build LayerRefs with the right namespace, and the internalSegment(line 105) maps each Layer to its namespace token, throwingArgumentOutOfRangeExceptionon an unmapped member (line 116).
- The constructor wraps
- Why it's built this way: a per-repo map stays a flat declaration of assemblies (the two abstract members), and everything derivable is derived, so the maps cannot drift in how they compute namespaces.
- Where it's used: each repo's concrete map subclasses this: CommonArchitectureMap (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/CommonArchitectureMap.cs:15), AdcArchitectureMap (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/AdcArchitectureMap.cs:14),StoreArchitectureMap(MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/StoreArchitectureMap.cs:8), andHelpdeskArchitectureMap(MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/HelpdeskArchitectureMap.cs:8), plus the private SpecTestMap fixture inside MMCA.Common's SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:40).FindRepoRootis called directly by every file-reading base and rule: DataResidencyTestsBase, FormsConventionTestsBase, FrameworkVersionConsistencyTestsBase, LocalizedTextConventionTestsBase, RawQueryableConventionTestsBase, StateManagementConventionTestsBase, UIArchitectureConventionTestsBase, the SortableColumnConventionTestsBase subclasses that build their own markup roots, and the proto ruleProtoContractsMatchFrozenList(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:46).
ConstructorDependencyCountTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:29· Level 3 · abstract class
- What it is: a single-responsibility-ceiling fitness function applied to three injection points a module composes: Application-layer
*Servicefacades, API controllers, and CQRS command/query handlers. It fails the build if any concrete class in a population has a constructor with more than that population's accepted dependency count. - Depends on: IArchitectureMap (via
Map.ModuleApplication()andMap.Api()in the default populations),[Fact], AwesomeAssertions, and reflection over constructors (System.Reflection.ConstructorInfo) and interfaces. - Concept introduced, quantifying the SRP smell across three populations.
[Rubric §1, SOLID]assesses single-responsibility discipline; a ballooning constructor-dependency list is the canonical smell wherever it appears, and this base turns a previously implicit judgement call into an enforced, per-population ceiling so the next class cannot silently grow past it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/ConstructorDependencyCountTestsBase.cs:5-28). Each ceiling is a ratchet, not a budget: a consumer sets it at its current high-water mark, raises it only as a recorded deliberate decision, and lowers it the moment remediation makes a lower number true (lines 14-19, enforced in both directions by the*_DoNotExceedConstructorDependencyCeilingand*_ConstructorDependencyCeilingIsTightfacts). What gets measured is itself an extension point: the three populations and the constructors measured on each type are virtual, with defaults that reproduce the module scan, so a consumer that overrides nothing scans exactly the module populations (lines 22-26). - Walkthrough: the subclass supplies
Map(line 37), plus one abstract ceiling per population:MaxConstructorDependenciesfor Application services (line 43),MaxControllerConstructorDependenciesfor API controllers (line 50), andMaxHandlerConstructorDependenciesfor CQRS handlers (line 58). Four virtual members say what is measured:ScannedServices(line 64) defaults to every concrete class whose name ends inServiceinMap.ModuleApplication()(lines 65-68).ScannedControllers(line 74) andScannedHandlers(line 81) default to the shared privateScan(line 240, flat-mapsConcreteClasses, excludes nested and compiler-generated types) overMap.Api()andMap.ModuleApplication(), filtered byIsController(line 257, matches the full nameMicrosoft.AspNetCore.Mvc.ControllerBaseviaHasBaseTypeStartingWith) orIsCommandOrQueryHandler(line 260, matches a generic interface whose definition's full name starts with theICommandHandler/IQueryHandlerprefix constants, lines 34-35).MeasuredConstructors(Type)(line 89) defaults totype.GetConstructors(), the public instance constructors (line 92); the widest measured constructor is the type's dependency count, computed by the privateWidestConstructor(line 230).- Three ceiling facts share one shape: materialize the population, assert non-vacuity, assert no offender.
ApplicationServices_DoNotExceedConstructorDependencyCeiling(line 96),Controllers_DoNotExceedConstructorDependencyCeiling(line 119) andHandlers_DoNotExceedConstructorDependencyCeiling(line 143) each assert non-vacuity first (lines 100-101, 123-124, 147-148) and report offenders by name and count through the shared instance helperOffenders(line 247), which measures each type withWidestConstructor(line 252) and so honors the overridableMeasuredConstructors. - Three tightness facts enforce the other direction of the ratchet:
ApplicationServices_ConstructorDependencyCeilingIsTight(line 168),Controllers_ConstructorDependencyCeilingIsTight(line 181) andHandlers_ConstructorDependencyCeilingIsTight(line 194) each delegate to the privateAssertCeilingIsTight(line 207). It asserts the population is non-empty, finds the observed maximum and the classes sitting at it, and fails when the declared ceiling exceeds that maximum; the message names the population, the ceiling property, its value, the observed maximum and the widest classes, so the fix (lower the ceiling to the observed number) is in the failure itself. - Controller and handler matching is by full type-name string, not a compiled reference to
Microsoft.AspNetCore.MvcorMMCA.Common.Application, so the rule library keeps zero compile dependency on either (lines 31-35).
- Why it's built this way: splitting the ceiling by population lets each stay honest: a controller is a thin translation layer over handlers, so a long injection list there means it serves more than one resource, while a handler serves exactly one use case and is the population where a long list is least defensible (lines 112-117, 136-141). The tightness facts exist because a ceiling left above the observed maximum is a budget, not a ratchet: the next class can grow into the slack without anything failing (lines 161-166). It overlaps the arity check in HandlerConventionTestsBase but scopes specifically to these three facades and lets a repo pin an exact number per population rather than take a shared default. Making the populations and the measured constructors virtual is what lets a repo without business modules adopt the same gate: per the summary doc, MMCA.Common overrides them to scan its framework assemblies (lines 22-23).
- Where it's used: MMCA.Common subclasses it over its own framework in FrameworkConstructorDependencyTests (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/ConstructorDependencies/FrameworkConstructorDependencyTests.cs:25): the populations are every top-level class in the Application and Infrastructure assemblies,ControllerBase-derived classes in the API assembly, and handler implementations in the Application assembly, abstract classes included (:42-49), andMeasuredConstructorswidens to public and protected constructors because the widest framework classes are abstract bases whose only constructors are protected (:51-53, rationale:8-12). Its ceilings are 7 for services (:33), 5 for controllers (:37, held byCrudEntityControllerBase,InboxControllerandUserAccountAuthControllerBase) and 7 for handlers (:40, held byResetPasswordHandlerBase). The base's own behavior is pinned by ConstructorDependencyCountTestsBaseTests. It is also subclassed in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:20) withMaxConstructorDependenciesat 9 (:24, held byAuthenticationService, history at:9-18),MaxControllerConstructorDependenciesat 10 (:52, held bySessionsController, history at:26-51), andMaxHandlerConstructorDependenciesat 7 (:61, held jointly byResetPasswordHandlerandSubmitQuestionHandler, rationale at:54-60). Store subclasses it with a service ceiling of 8, one below ADC because Store is local-credential only and has no external-login email verifier (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/ConstructorDependencyCountTests.cs:26, ceiling at:24, rationale at:8-18). MMCA.Helpdesk deliberately does not adopt it and records why in a comment: its Application layer is handlers-only, and the base's anti-vacuity guard fails when it finds no*Serviceat all (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:177-179).
ArchitectureRules
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:3· Level 4 · static partial class
- What it is: the reusable rule library: one large
static partial classsplit across forty-fourArchitectureRules.*.csfiles, whose methods each assert one architectural invariant across every applicable assembly a map declares. A repo's test classes reduce to a sealed subclass of the matching*TestsBasesupplying its own map. - Depends on: IArchitectureMap, Layer, ArchitectureAssert, RuleHelpers, CallGraphIndex, NetArchTest (
Types.InAssembly(...)),Mono.CecilplusMono.Cecil.Cilfor the IL-reading rules,System.Xml.Linqfor the props-file and.resxrules, source-generatedSystem.Text.RegularExpressionsfor the proto and localized-text parsers, and, for the specification rule,System.Linq.Expressionsplus CrossEntityNavigationFinder. - Concept introduced, the rule as a parameterized function. Each method takes an
IArchitectureMap(or, for the file-reading rules, a path and an allowlist) and does its own loop, so the*TestsBaseclasses are thin[Fact]shells that delegate. The partial is organized by concern across the filesArchitectureRules.{Ai, Anonymization, CancellationTokens, CascadeSoftDelete, ClockReads, CloudVendors, CommandValidators, Contracts, Controllers, Cycles, DeleteBehavior, DomainEventHandlerSaves, DomainThrows, Entities, ErrorCatalog, Events, FeatureFlags, FolderWidth, ForwardedJwt, Governance, HandlerResults, Handlers, Idempotency, Immutability, InlineStyles, IntegrationEventPurity, Layers, LifetimeTokens, Localization, LocalizedText, Markup, Modules, Naming, PiiInventory, Protos, Purity, QueryHandlerRepositories, Slices, SoftDelete, Specifications, StronglyTypedIds, Substitutability, Transport, Upcasters}.cs, filed under seven concern folders (Rules/Api,Rules/Contracts,Rules/Cqrs,Rules/Domain,Rules/Governance,Rules/Layering,Rules/Ui), so the rule library follows the same feature-by-folder convention it enforces.[Rubric §3, Clean Architecture],[Rubric §4, DDD],[Rubric §7, Microservices Readiness], and[Rubric §34, Architecture Governance]all apply: this is where the codebase's structural decisions become executable assertions. - Walkthrough: six representative shapes, each the template for a family of rules.
- NetArchTest shape,
ControllersDoNotDependOnInfrastructure(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.Controllers.cs:6): loops the map's per-module API layer refs, computes the forbidden Infrastructure namespace viamap.RootNamespace(...), runs aTypes.InAssembly(...)chain filtered to theControllersuffix, and reports throughArchitectureAssert.NoViolations(result, ...).ControllersDoNotDependOnEntityFrameworkCore(line 22) is the same loop against the literalMicrosoft.EntityFrameworkCorenamespace, keeping data access out of the API layer. - Layer-flow shape,
ArchitectureRules.Layers.cs: one public method per forbidden edge,DomainDoesNotDependOnApplication(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Layers.cs:12) throughUiDoesNotDependOnInfrastructure(line 60), all delegating to the privateLayerNotDependOnLayer(line 139), which loops every assembly of thefromlayer and asserts no dependency on thetolayer's namespace. Two non-vacuity rules sit alongside them,LayerMapDeclaresLayers(line 72) and the twoModulesDeclareLayersoverloads (lines 89 and 113). - Reflection shape,
ControllersAreSealed(ArchitectureRules.Controllers.cs:37): flat-mapsmap.Api()to each assembly'sConcreteClasses(the RuleHelpers extension property), filters non-sealed controllers via the privateIsController(line 70, which matches on theControllersuffix or an MVC base type), and asserts the string offender list is empty.ControllersInheritApiControllerBase(line 54) is the same shape with a caller-supplied exempt set, accepting either ApiControllerBase or EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> as the base. - Custom-rule shape,
ServiceContractsDoNotDependOnServiceInternals(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32): the one rule that hands NetArchTest aMeetCustomRulepredicate (line 44) reading Cecil metadata directly, because the selector is an attribute, not a name or a namespace. - IL-reading shape, the rules that must see inside a method body, because neither NetArchTest nor reflection can:
HardDeletesOnlyInAllowedTypes(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62),DomainThrowsOnlyArgumentGuards(ArchitectureRules.DomainThrows.cs:71), the two error-catalog rulesErrorCodesAreUniqueandErrorCodesUseAnAllowedPrefix(ArchitectureRules.ErrorCatalog.cs:63,:103),AggregatesCascadeSoftDeleteToChildren(ArchitectureRules.CascadeSoftDelete.cs:100), the transitiveDomainEventHandlersDoNotSave(ArchitectureRules.DomainEventHandlerSaves.cs:76),DomainAndApplicationDoNotReadTheClock(ArchitectureRules.ClockReads.cs:59), andQueryHandlersUseReadRepositories(ArchitectureRules.QueryHandlerRepositories.cs:37). They open each mapped assembly withMono.Cecil.ModuleDefinition.ReadModule(for exampleArchitectureRules.SoftDelete.cs:73overScannableAssemblyLocations, line 88;ArchitectureRules.ErrorCatalog.cs:170over its ownErrorCatalogAssemblyLocations, line 185) and match callees, base types and thrown exception types by full NAME, which is how the package keeps its zero-reference stance one level deeper than the reflection rules do. - Graph shape,
NamespacesHaveNoDependencyCycles(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45): builds a namespace-to-namespace graph per layer assembly from signature-level references (BuildNamespaceGraph, line 84), finds every strongly connected component (FindNamespaceCycles, line 253), and reports the shortest cycle path plus any extra members of the component. At 379 lines it is the second-largest rule file in the library, after the 438-lineArchitectureRules.Protos.cs.
- NetArchTest shape,
- Why it's built this way: ADR-015 records the intent: the rule bodies live once here, and each repo's architecture test project is a set of sealed subclasses supplying its map, so all four repos enforce identical rules. The compile-time
MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targetsguards the same layer flow at build time as a second, faster gate. - Where it's used: every
*TestsBasein this group calls into it; those[Fact]methods are its public surface. A handful of rules are also called directly from MMCA.Common's own fitness self-tests, for exampleBuildProtoContract/AssertProtoContractfrom ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14) and the cascade rule from CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17). - Caveats / not-in-source: the full method roster spans forty-four partials; only the entry file and representative methods are cited here. The authoritative fitness-method and base-class counts are generated into
MMCA.Common/FACTS.md:45-50and CI-gated, so read them there rather than counting by hand.
DataResidencyTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:14· Level 4 · abstract class
- What it is: a compliance-drift fitness function: the data-residency statement in a repo's
PRIVACY.mdmust match the region where personal data is actually provisioned, and known-stale region claims must not reappear. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.IO(File.ReadAllText), AwesomeAssertions. - Concept introduced, a document-versus-infrastructure consistency gate.
[Rubric §30, Compliance / Privacy / Data Governance]assesses whether privacy claims track reality; this base fails the build if either the deployed region or the privacy policy changes without the other, closing the gap where a policy once claimed a region the data never lived in (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DataResidencyTestsBase.cs:3-13). - Walkthrough: the subclass supplies
Map(line 16), the optionalForbiddenResidencyClaimslist (line 24), and implementsExtractDeployedRegion(repoRoot)(line 87) against its own source of truth (the doc cites ADC parsing the SQL region default out ofdeploy.ymland Store parsinginfra/DISASTER-RECOVERY.md). The single[Fact]PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion(line 27) locates the repo root viaFindRepoRooton the map's repo token (line 29), asserts the extracted region is non-blank (lines 32-33), readsPRIVACY.md(line 35), then asserts the policy states the region (line 37) and none of the forbidden claims (lines 40-44), both throughContainsRegionClaim.ContainsRegionClaim(policyText, regionClaim)(line 57) is aprotected staticwhole-token matcher. It normalizes both sides withNormalize(line 93: strips whitespace and upper-cases withToUpperInvariant, per CA1308, so "West US 2" matches the "westus2" token), then walks every occurrence and accepts one only when the next character is not a digit and the text before it does not end with one of theDirectionalPrefixesNORTH,SOUTH,EAST,WEST,CENTRAL(lines 64-79, prefixes at line 89). Sowestusdoes not match "West US 2" andcentralusdoes not match "South Central US" (lines 47-53), which keeps a forbidden region that is a prefix of the real one from failing the gate, and a stated region that is only a prefix of another from passing it.
- Where it's used: subclassed in Store (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/DataResidencyTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/DataResidencyTests.cs:12); the matcher itself is exercised byDataResidencyTestsBaseTestsin MMCA.Common (see per-project test rollup). Module-less MMCA.Common has no deployed region, and Helpdesk records it as N/A for reduced-scope reasons (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:180).
FormsConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:27· Level 4 · abstract class
- What it is: a UX-safety fitness function: every admin
*Create.razorform underSource/Modulesmust keep its unsaved-changes guard, dirty tracking, model-sourced validation and per-form error summary, and the Identity Profile page must keep its password validation, so those protections cannot silently regress. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.IOfile enumeration,System.Text.StringBuilder, AwesomeAssertions. - Concept introduced, the markup-scanning fitness function (it reads
.razortext, not assemblies).[Rubric §24, Forms / Validation / UX Safety]assesses whether navigate-away data loss and missing validation are prevented; the base checks for ten literal markers in the defaultRequiredMarkers(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/FormsConventionTestsBase.cs:70-82): theUnsavedChangesGuardcomponent, anIsDirtyAccessorbinding (through the live accessor, which pre-empts the one-render stale-IsDirtylag, a §19 concern), an_isDirtyfield, aMudFormelement, then the model-bridge set:Model="_model",Validation="@_validate", an<ErrorSummaryfed the failed save (Result="_saveResult") and the live form errors (Messages="_form?.Errors"), and the localizedValidation.CorrectFollowingheading. The old per-fieldRequired="true"andRequiredErrormarkers are deliberately gone from the default list: a form still spelling them on every field would be the regression, not the convention (doc at lines 62-69). The marker list isvirtual, so a repo can narrow or extend it. - Walkthrough: the subclass supplies
Map(line 46) and optionally a higherMinimumCreateFormscount (line 53, default 1) or a differentModulesRoot(line 59, virtual, defaultSource/ModulesunderArchitectureMapBase.FindRepoRooton the map's repo token, overridden to point the facts at a fixture tree). Three facts run over a shared privateDiscoverCreateForms(line 233), which enumerates*Create.razorunderModulesRootexcludingobjandbin, and asserts the discovered count meets the floor.AdminCreateForms_KeepUnsavedChangesGuardAndValidation(line 148) reads each form and records a violation naming each missingRequiredMarkersentry, found through the public staticMissingMarkers(line 138), which strips Razor and HTML comments first so a commented-out guard cannot stand in for one that renders.AdminCreateForms_ReadRequirednessOffTheirModel(line 180) checks the requiredness half of the bridge: the form's markup plus any sibling*FormFields.razorcomponent in the same folder must containModelValidation.IsRequired, so extracting the field block does not weaken the scan (lines 7-26 describe the[Required]attribute on the form model being read back into the markup). Each file has its Razor and HTML comments stripped BEFORE it joins the scan (lines 187 and 192), so a commented-outModelValidation.IsRequireddoes not count and an unterminated comment in one file cannot swallow the next, the same treatment the marker and password checks get (doc at lines 174-177).ProfileForm_KeepsErrorSummaryAndPasswordValidation(line 212) readsProfileFormPath(line 89; defaults toSource/Modules/Identity/{RepoToken}.Identity.UI/Pages/Users/Profile/Profile.razor,nullonly for a repo with no Profile page) and passes whenRendersChangePasswordCard(line 257) finds an uncommented<ChangePasswordCardtag (ChangePasswordCardTag, line 30; the shared privateStripAllComments, line 263, removes Razor and HTML comments first); otherwise the inline form must satisfyMissingPasswordFormMarkers(line 109, which also strips comments before matching, so commented-out markers and attributes do not count): the error summary, the live_passwordForm?.Errorsfeed,ValidateNewPasswordandValidateConfirmPasswordwiring (PasswordFormMarkers, line 38), plus at least threeRequired="true"and threeRequiredErroroccurrences. That method is public static so MMCA.Common can hold the sharedChangePasswordCardto the same list.
- Why it's built this way: self-service forms with no navigate-away step (for example a single-section Profile password or delete form) carry no guard by design and simply must not match the
*Create.razorglob; the Profile page gets its own fact instead (lines 22-24). - Where it's used: subclassed in the repos with admin create forms, Store (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/FormsConventionTests.cs:13) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/FormsConventionTests.cs:13). The base's marker helpers are pinned byFormsConventionTestsBaseTestsin MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionTestsBaseTests.cs), and the comment-stripping requiredness fact byFormsConventionRequirednessFitnessTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/FormsConventionRequirednessFitnessTests.cs:12), whose private subclass overridesModulesRootwith aRazorFixtures/Formstree (:38-43) and invokes the fact directly against a commented-out marker (:23) and a live marker in a sibling field component (:31).
FrameworkVersionConsistencyTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:13· Level 4 · abstract class
- What it is: an evolvability and drift fitness function that makes ADR-016 executable: all
MMCA.Common.*packages in a consumer'sDirectory.Packages.propsmust be pinned to one version, so a partial sweep is caught at CI time. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.Xml.Linq(XDocument), AwesomeAssertions. - Concept introduced, enforcing the lockstep release policy.
[Rubric §15, Best Practices & Code Quality]and[Rubric §32, Dependency & Supply-Chain]assess coordinated versioning; the framework releases in lockstep with no phased rollout (ADR-016), and this gate fails if anyMMCA.Common.*entry diverges (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FrameworkVersionConsistencyTestsBase.cs:3-11). - Walkthrough: the subclass supplies
Map(line 15) and optionallyMinimumCommonPackageCount(line 22, default 13).AllMmcaCommonPackages_ArePinnedToOneVersion(line 25) loadsDirectory.Packages.propsfrom the repo root (lines 27-30), selects everyPackageVersionelement whoseIncludestarts with theMMCA.Common.prefix (lines 31-41), asserts the count meets the floor (lines 43-44), asserts none has an empty version (lines 46-48), and asserts the distinct-version count is exactly one, listing what it found (lines 50-56). - Why it's built this way: MMCA.Common itself does not subclass this, because it declares no
MMCA.Common.*pins; only consumers do (lines 10-11). The default floor of 13 is deliberately loose, and the doc points atMMCA.Common/FACTS.mdfor the authoritative released-package count (lines 17-21; that count is 17 today, generated atMMCA.Common/FACTS.md:19). - Where it's used: subclassed in Store (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FrameworkVersionConsistencyTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:47). None of the three overrides the floor today, so all three run against the shared default of 13.
RawQueryableConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:30· Level 4 · abstract partial class
- What it is: an opt-in extraction-readiness gate: Application-layer code must not use the repository's raw
IQueryablesurfaces (Table,TableNoTracking,TableNoTrackingSingleQuery,TableNoTrackingSplitQueryon IReadRepository<TEntity, TIdentifierType>), because a handler written against a raw queryable is EF-coupled and its query shape cannot cross a gRPC boundary. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot, ArchitectureAssert,System.IOenumeration, and a source-generatedRegex(System.Text.RegularExpressions). - Concept introduced, the honest textual scan (and its stated limits). The doc is unusually candid about the tradeoff (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawQueryableConventionTestsBase.cs:13-23): NetArchTest and plain reflection cannot see member usage inside method bodies, and this base deliberately carries no IL or Roslyn dependency, so the rule reads.cstext instead. It cannot see through variable indirection (an interface alias re-exposing the queryable is missed), and because it skips only whole-line comments, a match inside a string literal or trailing comment is a rare false positive.[Rubric §7, Microservices Readiness]is the invariant being protected;[Rubric §8, Data Architecture]and[Rubric §15, Best Practices & Code Quality]apply to the handler style it pushes toward (focused repository methods, readers, queriers, specifications). - Walkthrough
AllowedFiles(line 38) is the adoption ratchet: a repo with existing violations subclasses, runs once, and moves the reported file names in, so new files stay clean while the list shrinks (lines 24-28).ApplicationSourceDirectories()(line 45) defaults to locating each declared module's Application project directory under the repo'sSource/tree by project name (lines 47-57), and isvirtualfor a custom layout.- The
[Fact]ApplicationLayer_DoesNotUseRawQueryableSurfaces(line 61) first asserts the directory list is non-empty, with a message telling the author to override the directory hook (lines 65-66), then enumerates every.csfile, skippingobj,bin, and allowlisted file names (lines 71-79), and reports throughArchitectureAssert.NoViolations(line 84). ScanFile(line 88) yields a file-name, line-number and trimmed-line triple for every non-comment line matchingRawQueryableAccessRegex(line 103), a[GeneratedRegex]partial property matching theTableandTableNoTrackingmember-access family with a 2000 ms match timeout.
- Why it's built this way: making the offender message carry the file and line (lines 96-98) is what makes a textual gate actionable; combined with the allowlist ratchet it can be adopted in a repo that is not yet clean. Note the contrast with the IL-reading rules in ArchitectureRules, which do read method bodies: a member read on a property is what a text scan is cheapest at, while a call is what Cecil sees best.
- Where it's used: subclassed opt-in in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:13), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawQueryableConventionTests.cs:11). Because it is a ratchet, a repo'sAllowedFilesoverride is the record of its remaining debt.
RawSqlConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:31· Level 4 · abstract partial class
- What it is: an opt-in extraction-readiness gate: module code must reach SQL only through the parameterizing overloads, never
FromSqlRaw,SqlQueryRaw,ExecuteSqlRaw, orExecuteSqlRawAsync, whose plain-stringparameter lets an interpolated value be concatenated straight into the statement instead of being parameterized. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot, ArchitectureAssert,System.IOfile enumeration, and a source-generatedRegex(System.Text.RegularExpressions). - Concept introduced, the same textual-scan shape as RawQueryableConventionTestsBase, aimed at a SQL-injection risk rather than an EF-coupling one.
[Rubric §11, Security]assesses injection defenses directly;[Rubric §7, Microservices Readiness]and[Rubric §8, Data Architecture]apply to the parameterized alternatives it pushes toward,FromSql/SqlQuery/ExecuteSqlor the framework'sIRawSqlQueryExecutor, whoseFormattableStringparameters cannot be concatenated (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/RawSqlConventionTestsBase.cs:90-94). - Walkthrough
AllowedFiles(line 39) is the adoption ratchet, empty by default: a repo with existing raw-SQL call sites lists the file names here.ScannedSourceDirectories()(line 48) defaults to every project directory the map attributes to a business module, located by project name under the repo'sSource/tree viaFindRepoRoot(lines 50-58), and isvirtualfor a custom layout or to scan a framework project too.- The
[Fact]ModuleCode_UsesParameterizedSqlOnly(line 65) first asserts the scanned-directory list is non-empty, with a message pointing at the override hook (lines 70-72), then enumerates every.csfile, skippingobj,bin, and allowlisted file names (lines 75-86), and reports throughArchitectureAssert.NoViolations(lines 90-94). ScanFile(line 100) yields a file-name, line-number and trimmed-line triple for every non-comment line matchingRawSqlAccessRegex(lines 105-110), a[GeneratedRegex]partial property matching theFromSqlRaw/SqlQueryRaw/ExecuteSqlRaw/ExecuteSqlRawAsyncmember-access family with a 2000 ms match timeout (lines 115-116).
- Why it's built this way: mirrors RawQueryableConventionTestsBase's honest-textual-scan tradeoff (no IL or Roslyn dependency, cannot see through variable indirection), but where that base protects extraction readiness this one protects against injection: the offender message names the file and line so the finding is actionable, and the allowlist ratchet lets a repo with existing debt adopt the gate without going clean first.
- Where it's used: subclassed opt-in in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/RawSqlConventionTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/RawSqlConventionTests.cs).
StateManagementConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:18· Level 4 · abstract partial class
- What it is: a Blazor Server state-management gate: user and session state must live in per-circuit scoped services, never in mutable
staticmembers (which leak one user's state to another) or in singleton-registered stateful services. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot, reflection over the UI assemblies, aSource/file scan, source-generatedSystem.Text.RegularExpressions([GeneratedRegex], hencepartial), andSystem.Runtime.CompilerServices.CompilerGeneratedAttribute. - Concept introduced, a reflection plus source-scan combined gate.
[Rubric §19, State Management]assesses per-circuit state safety; Blazor Server shares one process across every circuit, so a static member is shared across every user (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:6-17). - Walkthrough: the subclass supplies
Map(line 20, whose UI assemblies must be registered underLayer.Ui) and optionallyAllowedStaticMembers(line 26).UiAssemblies_CarryNoMutableStaticState(line 29) reflects overMap.OfLayer(Layer.Ui), first asserting the set is non-empty (lines 33-34), then skipping enums, interfaces, and compiler-generated types (line 41) before flagging any declared static field that is notreadonly, notconst, and not compiler-generated, plus any settable static property, minus the exempted members (lines 46-57).UiProjects_RegisterStatefulServicesScoped(line 67) resolves the repo'sSource/directory, delegates toFindSingletonStateRegistrations(line 72), and asserts two things: that at least one UI file was scanned, so the gate cannot pass by seeing nothing (lines 74-75), and that no offender was found (lines 76-78).FindSingletonStateRegistrations(sourceDir, out scannedUiFiles)(line 92) isprotected static, so the scan is testable on its own. It judges each.cspath RELATIVE tosourceDir, directory segment by segment: a file counts only when one segment matchesUiProjectSegment(a*.UIor*.UI.*project directory, line 121) and no segment isbin/objor hasTestingas a whole dot-separated part (IsTestingSegment, line 128, soMMCA.Common.Testing.UIis skipped butTestingHelpersandLoadTestingare still scanned; check at lines 102-103), so where the checkout lives cannot skip or include it. It then runsSingletonStateRegistration(line 140; the pattern at lines 136-139) over the whole file text. The regex is bounded byAddSingleton's own type-argument list or its own argument list (the argument branch tracks parenthesis nesting depth), so the generic, factory,typeofand instance forms and a registration wrapped across lines all match, while a call chained after it (for example.AddScoped<IWizardStateService, ...>()) is out of reach; each match becomes aFile.cs:lineoffender computed from the match index (loop at line 110). Both[GeneratedRegex]properties carry a 1000 ms match timeout (lines 120, 139).- The private
GetLoadableTypes(line 142) repeats the tolerant load locally rather than using the internal RuleHelpers, andIsCompilerGenerated(line 154) treats any member name containing an angle bracket as generated, or one carryingCompilerGeneratedAttribute.
- Where it's used: subclassed in the repos with Blazor UI assemblies: MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/StateManagementConventionTests.cs:10), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9).
UIArchitectureConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:14· Level 4 · abstract class
- What it is: a UI-architecture convention gate holding the container and presentational split with two mechanical line-count caps: a
*.razor.cscode-behind stays withinMaxCodeBehindLines, and a.razorfile's inline@codeblock stays withinMaxInlineCodeLines. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot,System.IOfile enumeration, AwesomeAssertions. - Concept introduced, enforcing a design convention by file metrics.
[Rubric §18, UI Architecture]assesses the container/presentational discipline; a ballooning code-behind signals page logic that belongs in an injected UI service or an extracted sub-component, and putting a number on it moves the judgement from review to CI (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/UIArchitectureConventionTestsBase.cs:3-13). - Walkthrough: the subclass supplies
Map(line 16); the capsMaxCodeBehindLines(line 22, default 400),MaxInlineCodeLines(line 29, default 120),MinimumCodeBehindFiles(line 35, a non-vacuity floor, default 1), andExcludedPathFragments(line 41) are all overridable, and the doc says the caps should move only with a recorded decision (lines 11-12).CodeBehinds_StayWithinTheLineCap(line 44) enumerates*.razor.cs, asserts the floor (lines 48-49), and flags files over the cap with their line counts (lines 51-59).RazorFiles_KeepInlineCodeBlocksSmall(line 63) finds each.razorfile's@codeline and measures the tail block from there to end of file (line 77), flagging it when it exceeds the inline cap.EnumerateSourceFiles(line 89) drives both, resolving the repo root and excludingobj,bin, and the excluded fragments.
- Caveats / not-in-source: the inline-
@codemeasurement assumes the block is the file's tail (stated in the doc at lines 25-28), so a.razorfile with markup after its@codeblock would over-count. - Where it's used: subclassed in the repos with Blazor UI: MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:9), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/UIArchitectureConventionTests.cs:10).
AggregateConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:10· Level 5 · abstract class
- What it is: the minimal DDD aggregate fitness base for repos with no business modules (MMCA.Common itself): it asserts the Domain layer exposes aggregate roots, each built through a static
Create(...)factory returningResult<T>with no public constructor, and that every domain or value-object factory returns aResult. - Depends on: IArchitectureMap, ArchitectureRules,
[Fact]. - Concept introduced, the thin delegating test base shared by most Level-5 types in this group: a
protected abstract IArchitectureMap Mapplus one[Fact]per rule that forwards to an ArchitectureRules method, with no logic of its own. The factory-returning-Result idiom on AuditableAggregateRootEntity<TIdentifierType> is what these rules verify.[Rubric §4, DDD]assesses aggregate discipline. - Walkthrough: four
[Fact]s, each a one-line delegate:Domain_ShouldExpose_AggregateRoots(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/AggregateConventionTestsBase.cs:15),AggregateRoots_ShouldHave_ResultReturningCreateFactory(line 18),AggregateRoots_ShouldHave_NoPublicConstructors(line 21, which targets the framework-specificDomainAggregateRootsHaveNoPublicConstructorsrule atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Entities.cs:90rather than the module-scoped one at line 167), andDomainFactories_ShouldReturn_Result(line 24, delegating toArchitectureRules.DomainFactoriesReturnResult,ArchitectureRules.Entities.cs:53). - Why it's built this way: module-bearing repos use the fuller EntityConventionTestsBase instead (doc, lines 7-8); this base exists so a module-less repo still guards its aggregates.
- Where it's used: subclassed only in MMCA.Common's architecture test project (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/AggregateConventionTests.cs:9).
AiDependencyIsolationTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19· Level 5 · abstract class
- What it is: a two-rule delegating base enforcing the governed AI boundary: LLM-provider SDKs must not leak past the layer that governs them, and the framework's own governed-AI package must stay behind Infrastructure rather than be referenced directly from Domain, Application, or UI code.
- Depends on: IArchitectureMap, ArchitectureRules
.LanguageModelSdksStayAtTheModelBoundaryand.GovernedAiPackageStaysBehindInfrastructure. - Concept: the same thin delegating-base shape as AggregateConventionTestsBase and ConcurrencyConventionTestsBase: a
protected abstract IArchitectureMap Mapplus one[Fact]per rule, no logic of its own. ADR-120 is the boundary this base makes executable.[Rubric §3, Clean Architecture]and[Rubric §7, Microservices Readiness]assess whether a provider-specific dependency stays isolated at one layer. - Walkthrough: two
[Fact]s, each a one-line delegate (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/AiDependencyIsolationTestsBase.cs:19-30):LanguageModelSdks_ShouldStayAt_TheModelBoundary(line 24) forwards toArchitectureRules.LanguageModelSdksStayAtTheModelBoundary(Map)(line 25), andGovernedAiPackage_ShouldStayBehind_Infrastructure(line 28) forwards toArchitectureRules.GovernedAiPackageStaysBehindInfrastructure(Map)(line 29). - Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/AiDependencyIsolationTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/AiDependencyIsolationTests.cs).
CancellationTokenConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:16· Level 5 · abstract class
- What it is: a cancellation-uniformity gate: every public async method on a public Application- or Infrastructure-layer type must declare a trailing
CancellationToken cancellationTokenparameter. - Depends on: IArchitectureMap and ArchitectureRules
.AsyncMethodsDeclareTrailingCancellationToken(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CancellationTokens.cs:35). - Concept introduced, uniformity as the invariant (not presence). The doc states the reasoning (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CancellationTokenConventionTestsBase.cs:4-9): cancellation is only end-to-end if it is uniform. One method that swallows the token turns a cancelled request, an expired CQRS timeout budget, or a stopping host into work that keeps running against the database, and a token in a non-trailing position or under a different name defeats the mechanical forwarding the decorator pipeline and the repositories rely on. That is why the rule reports three distinct problems, not one: a missing token, a token that is not the last parameter, and a trailing token under the wrong name (ArchitectureRules.CancellationTokens.cs:115).[Rubric §12, Performance & Scalability]assesses whether abandoned work is actually abandoned;[Rubric §29, Resilience & Business Continuity]assesses graceful shutdown behavior;[Rubric §15, Best Practices & Code Quality]covers the async convention itself. - Walkthrough
- The subclass supplies
Map(line 19) and optionallyCancellationTokenExemptMethods(line 26, empty by default), entries in"TypeName.MethodName"form. The doc is deliberately narrow about when to use them (lines 21-25): a shipped public API where adding the parameter would break consumers, each entry justified by a comment. - The single
[Fact]AsyncMethods_ShouldDeclare_TrailingCancellationToken(line 29) forwards both to the rule. - The rule itself carries the interesting mechanics. Scope is methods returning
Task,Task<T>,ValueTask, orValueTask<T>(IsAwaitableReturn,ArchitectureRules.CancellationTokens.cs:105) declared on a publicly-visible, non-delegate, non-compiler-generated type (IsCandidateType, line 92).ExternallyFixedMethods(line 141) computes the signatures the repo does not own: overrides whose base definition is declared outside the map's assemblies, and implicit implementations of an interface declared outside the map (resolved throughInterfaceTargets, line 178, with unmappable generic interfaces swallowed).Dispose/DisposeAsyncand special-name members are excluded outright (line 97).
- The subclass supplies
- Why it's built this way: exempting only what is externally fixed keeps the gate honest. A framework contract such as
IHostedService.StartAsynccannot grow a parameter, but a first-party service method can, so it is not excused just for implementing an interface. - Where it's used: subclassed today only in MMCA.Common, as CancellationTokenConventionTests (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/CancellationTokenConventionTests.cs:10), which exempts exactly two methods:NotificationHub.JoinChannelAsyncandNotificationHub.LeaveChannelAsync(:23-27). Its recorded reason is that a SignalR hub method signature is the client-visible RPC contract bound by name and argument list, and both methods already passContext.ConnectionAbortedinto the group calls, so the work is cancellable through a path the rule cannot see (:14-22; the hub itself is NotificationHub).
CascadeSoftDeleteConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/CascadeSoftDeleteConventionTestsBase.cs:18· Level 5 · abstract class
- What it is: a delegating base for the cascade-soft-delete rule: an aggregate root that owns child entities must delete them in its own
Delete()override, because a soft delete is an ordinary UPDATE and nothing cascades for free. - Depends on: IArchitectureMap and ArchitectureRules
.AggregatesCascadeSoftDeleteToChildren(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.CascadeSoftDelete.cs:100), which reads IL through CallGraphIndex. - Concept introduced, the invariant a soft delete quietly breaks. Setting
IsDeleted = trueon a root hides the root behind the global query filter and leaves every child row ACTIVE: an order line of a deleted order, a session of a deleted event. Nothing reads those rows through the root any more, so the orphans stay invisible until a report, an export or a per-child query surfaces them, and an erasure request that walks the child table finds data whose owner was deleted months ago. The database cannot help, because there is noON DELETE CASCADEto fire on an UPDATE (rule doc,ArchitectureRules.CascadeSoftDelete.cs:35-50).[Rubric §4, DDD]assesses whether an aggregate really owns its children's lifecycle;[Rubric §8, Data Architecture]and[Rubric §30, Compliance / Privacy / Data Governance]assess orphaned rows surviving into exports and erasure accounting (ADR-005). - Walkthrough
- The subclass supplies
Map(line 20) and optionallyAllowedCascadeExemptTypes(line 27, type full names or namespace prefixes; empty by default, which requires every child-bearing aggregate to cascade). The doc frames the list as the point of the rule: it turns "delete cascades, mostly" into a reviewed inventory of every aggregate that leaves children behind on purpose (lines 10-16). - The single
[Fact]AggregatesWithChildCollections_MustCascadeSoftDelete_InDelete(line 30) forwards both to the rule. - In the rule, a child collection is an instance field declared on the aggregate whose type is one of six generic collection types (
ChildCollectionTypeNames,ArchitectureRules.CascadeSoftDelete.cs:25-33) over an element type deriving from the auditable-entity base. Auto-properties are covered by the same pass, because an auto-property is a compiler-generated instance field whose backing-field name is normalized back to the property name for the report (doc, lines 61-69). - What counts as cascading is equally precise (doc, lines 70-78): a direct call inside the
Delete()override toDeleteChildren, or to a zero-argumentDeleteon something other thanthis, which is what a hand-rolled loop over the children compiles to.base.Delete()is deliberately NOT accepted, because C# emits it as a non-virtualcallto a virtual method, a shape nothing else produces, so the rule can tell "delete myself" from "delete my children" without guessing.
- The subclass supplies
- Why it's built this way: the rule reads IL because neither NetArchTest nor reflection can see a call inside a method body, and it matches bases and callees by full name with the generic arity stripped, so the package keeps its zero-reference stance and still recognizes an aggregate whose base lives in an unregistered assembly (doc, lines 52-60). Four limits are stated outright (lines 84-93): only direct calls in the override are read, the rule proves a cascade EXISTS rather than that it covers every collection, only fields declared on the aggregate are inspected, and grandchildren are the child's own cascade to own.
- Where it's used: subclassed in all three repos with aggregates plus the framework. ADC exempts exactly one type,
Speaker, whose junction rows deliberately outlive the soft-deleted root because the Sessionize import reactivates them in place (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12, exemption and reasoning at:17-26). Store exemptsCategory, which REFUSES the delete instead of cascading it (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:12,:17-25). MMCA.Common runs it as a ratchet over its own module-lessSource/, which declares no child-bearing aggregate today (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteConventionTests.cs:14); the rule's own behavior is proven against compiled fixtures in CascadeSoftDeleteFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/CascadeSoftDeleteFitnessTests.cs:17).
ClockReadTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15· Level 5 · abstract class
- What it is: a delegating base for the ambient-clock rule: Domain and Application code must not read the wall clock directly, so behavior stays deterministic and testable through an injected time source instead.
- Depends on: IArchitectureMap and ArchitectureRules
.DomainAndApplicationDoNotReadTheClock. - Concept: the same thin delegating-base shape as AggregateConventionTestsBase and AiDependencyIsolationTestsBase: a
protected abstract IArchitectureMap Mapplus one[Fact]forwarding to an ArchitectureRules method, with no logic of its own. - Walkthrough: the subclass supplies
Map(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ClockReadTestsBase.cs:15) and optionallyAllowedClockReaders(line 22, empty by default): type full names, namespace prefixes, or single members (MMCA.X.Sales.Domain.Orders.Order.RepublishFulfillment) where reading the clock is the deliberate, reviewed choice. The single[Fact]DomainAndApplication_ShouldNotReadTheAmbientClock(line 25) forwards both to the rule. - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ClockReadTests.cs:2) and MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ClockReadTests.cs:2).
CloudVendorIsolationTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:15· Level 5 · abstract class
- What it is: the cloud-vendor fitness function (ADR-134): the assemblies a repo declares cloud-neutral name no Azure, AWS or Google Cloud SDK type, reference no vendor SDK or vendor broker-transport assembly, and never reference a vendor adapter package (
MMCA.Common.Azure,MMCA.Common.Aws,MMCA.Common.Gcp,MMCA.Common.Testing.Azure) (doc,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/CloudVendorIsolationTestsBase.cs:3-7). - Depends on: two ArchitectureRules members,
CloudNeutralAssembliesNameNoVendorType(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.CloudVendors.cs:74) andCloudNeutralAssembliesReferenceNoVendorAssembly(:107), the first reporting through ArchitectureAssert. Like DependencyVersionTestsBase it takes no IArchitectureMap: the subclass hands over raw assemblies, because the set it guards (service defaults, test packages) is wider than any map's layers. - Concept introduced, the runtime half of a two-gate boundary. The compile-time half is the
EnforceCloudVendorBoundarytarget (MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets:130), which judges the declared package references; this base judges the compiled assemblies, so a vendor type that arrives through a transitive path is caught too (doc, lines 8-12). The rule doc names what is at stake: adapters reference the core, and the reverse edge turns "runs on any cloud" into "pulls one cloud's SDK, credential chain and advisory stream into every consumer" (ArchitectureRules.CloudVendors.cs:21-22).[Rubric §7, Microservices Readiness]and[Rubric §3, Clean Architecture]assess whether infrastructure choices stay at the edges;[Rubric §32, Dependency & Supply-Chain]assesses what every consumer is forced to pull in. It sits beside AiDependencyIsolationTestsBase, the same isolation shape for the AI packages. - Walkthrough
- The subclass supplies
CloudNeutralAssemblies(line 18) and optionallyAllowedVendorPrefixes(line 24): per assembly name, the vendor prefixes it may still name, meant for a database engine such as Cosmos DB and never a cloud service. It defaults to an empty ordinal dictionary (line 25). - Two
[Fact]s forward both:CloudNeutralAssemblies_ShouldNotName_ACloudVendorType(line 28) andCloudNeutralAssemblies_ShouldNotReference_ACloudVendorAssembly(line 32). - The type-level rule runs NetArchTest
HaveDependencyOnAnyagainstCloudVendorDependencies(ArchitectureRules.CloudVendors.cs:25-41:Azure,Microsoft.Azure,Amazon,Google,FirebaseAdmin,Grpc.Auth, the two MassTransit vendor transports, and the four adapter packages). The entries are whole namespace roots with no trailing dot, because NetArchTest matches by namespace segment and an entry ending in.matches nothing, which would let the rule pass vacuously (:14-19,:27-28). When an assembly has an allowance, only its types that do NOT depend on an allowed prefix are checked (:86-88). - The reference-level rule reads
GetReferencedAssemblies()and flags any name matchingCloudVendorAssemblyPrefixes(:48-62, plain string prefixes such asAWSSDK.andGoogle.) or containing.Azure.or ending in.Azure(IsCloudVendorAssembly,:133-136), minus the assembly's allowed prefixes (:117-122).
- The subclass supplies
- Why it's built this way: two rules, not one, because they fail differently. The type rule names the offending types; the reference rule still holds the whole assembly to the allowance, so excusing the types that use Cosmos does not excuse a stray vendor reference elsewhere in the same assembly (rule doc,
:68-73).Googledeliberately also matchesGoogle.Protobuf; the doc says that if a cloud-neutral assembly ever needs it, the fix is one exact exemption, not a narrower Google entry (:16-18). - Where it's used: subclassed only in MMCA.Common, as
CloudVendorIsolationTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/CloudVendorIsolationTests.cs:17, rolled up in the per-project rollup), which holds every layer ofCommonArchitectureMapplusMMCA.Common.Aspire, the Testing and Testing.Aspire packages and Testing.Architecture itself (:19-26), with exactly one allowance:MMCA.Common.Infrastructuremay nameMicrosoft.Azure.Cosmos, because Cosmos DB is one of the four EF engines rather than a cloud service a host opts into (:28-32, reasoning at:11-15). The rules themselves are proven against real vendor SDK assemblies in the per-adapterCloudVendorRulesTests(for exampleMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudVendorRulesTests.cs:30,:38).
CommandValidatorCoverageTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:22· Level 5 · abstract class
- What it is: a validation-coverage gate: every command that carries data and has a handler must be covered by a validator, either its own
IValidator<TCommand>or the request-validator bridge. - Depends on: IArchitectureMap and two ArchitectureRules members,
CommandsHaveValidators(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.CommandValidators.cs:48) andHandledCommandCount(:80), plus AwesomeAssertions for the count assertion. - Concept introduced, gating a pipeline stage that has nothing to run. The Validating decorator runs FluentValidation before the transaction opens, so a command with no validator carries whatever the caller sent straight into the handler: the stage exists, it simply has nothing to execute, and the gap is invisible until bad input reaches the domain (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/CommandValidatorCoverageTestsBase.cs:4-7). The decorator pipeline itself is taught in primer §2.[Rubric §6, CQRS & Event-Driven]assesses the completeness of the pipeline's stages;[Rubric §11, Security]and[Rubric §24, Forms / Validation / UX Safety]assess whether untrusted input is checked before it acts. - Walkthrough
- The subclass supplies
Map(line 24) and optionallyAllowedUnvalidatedCommands(line 31, empty by default) andMinimumCommands(line 38, default 1). - Two
[Fact]s:Commands_ShouldHave_ValidationCoverage(line 41) forwards to the rule, andCommandInventory_ShouldNotBe_Empty(line 45) asserts the handled-command count meets the floor, so a map that resolves to no module Application assemblies cannot let the gate pass without reading anything (lines 46-48). - In the rule, a command is the command argument of every
ICommandHandlerimplemented in the map's per-module Application assemblies, restricted to closed types with at least one public settable property; aninitsetter counts, so the usual positional-record command is inspected, and a payload-free marker command is skipped (ArchitectureRules.CommandValidators.cs:96-102,:135). - Coverage means one of two things (
:112): a concreteIValidator<TCommand>, or the bridge, where the command implementsICommandWithRequest<TRequest>AND a concreteIValidator<TRequest>exists. Both halves of the bridge are required on purpose, because the framework registers the bridge validator for every such command whether or not a request validator resolves, and one that resolves none adds no rules (doc, lines 8-16). - Where it looks mirrors the DI reality (
:31-36): both commands and validators are read from the module Application assemblies, because the framework registers validators from the module assembly, so a validator living anywhere else would not resolve at run time and must not count as coverage here either.
- The subclass supplies
- Why it's built this way: interfaces are matched by open-generic full NAME through a shared
ClosedGenericArgumentsprojection (:122), which keeps the package free of a compile reference to FluentValidation and to the framework's own Application layer. Two limits are stated (:37-41): a validator declared for a base command does not cover its derived commands, matching FluentValidation's closed-type resolution, and abstract validators are ignored since nothing registers them. - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:18), Store, whose allowlist is grouped by why each entry is there (identifier-only commands and two false positives the module-scoped scan cannot see through, with the frozen-debt group now empty,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/CommandValidatorCoverageTests.cs:17, rationale at:8-15), and Helpdesk, which deliberately keepsAllowedUnvalidatedCommandsempty and writes real validators instead, because an allowlist entry names a type that a generated app or a scaffolded slice would never inherit (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:163, reasoning at:150-162, floor of 6 against 7 discovered commands at:173).
ConcurrencyConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:9· Level 5 · abstract class
- What it is: a one-rule delegating base asserting that no
*UpdateRequestimplements IConcurrencyAware, because the optimistic-concurrency token travels in theIf-Matchrequest header and not in the body. - Depends on: IArchitectureMap, ArchitectureRules.
- Concept: cross-references the delegating-base shape from AggregateConventionTestsBase. The invariant is a single-source-of-truth one: a token in the request body would give the same conditional-update check a second, competing source (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ConcurrencyConventionTestsBase.cs:3-8).[Rubric §8, Data Architecture]assesses optimistic-concurrency handling (ADR-035);[Rubric §9, API & Contract Design]assesses keeping a precondition in the one place HTTP defines for it. - Walkthrough: one
[Fact]UpdateRequests_ShouldNotImplement_IConcurrencyAware(line 13) delegating toArchitectureRules.UpdateRequestsAreNotConcurrencyAware(Map)(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:24). The rule scans the map's module Application assemblies for classes or value types whose RuleHelpers.SimpleNameends withUpdateRequestand reports any that implement the full-name-matched contract (lines 26-34). The base's doc notes that modules with no mutable aggregate are legitimately vacuous (line 7). - Where it's used: subclassed in all four repos: MMCA.Common, where the module-less run is a ratchet that fires the moment the framework itself grows a body-token update request (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:13, rationale at:5-11), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ConcurrencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:56).
ContractImplementationTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ContractImplementationTestsBase.cs:20· Level 5 · abstract class
- What it is: an encapsulation gate for the
[ServiceContract]boundary: the interface is the published surface of a service, so the concrete class serving it must not be public. - Depends on: IArchitectureMap and ArchitectureRules
.ServiceContractImplementationsAreNotPublic(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:81). - Concept introduced, the other half of a contract boundary. ServiceContractPurityTestsBase keeps the producer's internals out of the contract; this base keeps the contract's implementation out of the consumer's reach (doc, lines 10-13). A public implementation lets a consumer reference, construct or subclass the type, and each of those references is a coupling the extraction has to sever later, because an interface can be answered over a wire and a class cannot (lines 4-8, ADR-007).
[Rubric §7, Microservices Readiness]assesses whether an extraction stays reversible;[Rubric §1, SOLID](DIP) and[Rubric §9, API & Contract Design]assess binding to abstractions rather than to concretes. - Walkthrough: the subclass supplies
Map(line 22) and optionallyAllowedPublicImplementations(line 30, type full names or namespace prefixes for a shipped default a consumer is meant to construct, or a test double in a testing package). The single[Fact]ServiceContractImplementations_ShouldNotBe_Public(line 33) forwards both to the rule. In the rule, visibility is judged withType.IsVisible(ArchitectureRules.Contracts.cs:92), so a public class nested inside an internal one is correctly treated as unreachable and passes; only concrete classes are scanned, so an abstract base carrying shared behavior for several implementations is exempt by construction (doc, lines 63-68).ServiceContractInterfaceOf(line 116) returns the first marked interface a type implements, matched by the attribute's full name throughCarriesServiceContractAttribute(line 143). - Why it's built this way: like its twin, the rule is attribute-driven rather than Layer
.Contracts-driven and honest about the vacuous case (doc, lines 14-18): a repo that marks no interface passes without asserting anything, and the value is the ratchet, with no test to remember to write when the first contract appears. - Where it's used: subclassed in all four repos: MMCA.Common as a ratchet, since the framework marks no interface (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ContractImplementationTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ContractImplementationTests.cs:20), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ContractImplementationTests.cs:12), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ContractImplementationTests.cs:12).
ControllerConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:7· Level 5 · abstract class
- What it is: the presentation-layer convention base: controllers are thin and sealed, never reach Infrastructure or EF Core directly, and inherit the framework ApiControllerBase for consistent Result-to-HTTP mapping.
- Depends on: IArchitectureMap, ArchitectureRules.
- Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a
protected virtual ControllersExemptFromApiControllerBaselist (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ControllerConventionTestsBase.cs:12) for controllers that legitimately bypass the base (for example a webhook endpoint that owns its own response semantics).[Rubric §9, API & Contract Design]assesses consistent controller shape. - Walkthrough: four
[Fact]s:Controllers_ShouldNotDependOn_Infrastructure(line 15),Controllers_ShouldNotDependOn_EntityFrameworkCore(line 18),Controllers_ShouldBe_Sealed(line 21), andControllers_ShouldInherit_ApiControllerBase(line 24, passing the exempt list). The underlying rules live inArchitectureRules.Controllers.csat lines 6, 22, 37 and 54. - Where it's used: subclassed in every repo with business modules: Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Api/ControllerConventionTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/ControllerConventionTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:84).
DeleteBehaviorConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DeleteBehaviorConventionTestsBase.cs:21· Level 5 · abstract class
- What it is: a cascading-delete convention base (ADR-119): a foreign key that cascades must say so explicitly in code, and every foreign key left to convention must resolve to a restrictive delete.
- Depends on:
ArchitectureRules.CascadingForeignKeysAreExplicitlyOptedInandArchitectureRules.ConventionForeignKeysRestrictDeletes. Unlike most bases in this family it takes no IArchitectureMap; the subclass instead supplies the finalized EF Core model directly. - Concept introduced, judging a live model instead of IL. The base's
Modelis typedobjecton purpose: the testing package deliberately carries no EF Core reference (doc, lines 28-29), so the subclass passesdbContext.Modeland the rule inspects it through reflection rather than a compile-time EF Core dependency. This pairs withRestrictDeleteByDefaultConvention, the model-finalizing convention that makesDeleteBehavior.Restrictthe default for every relationship unless a mapping calls.OnDelete(DeleteBehavior.Cascade)(MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Conventions/RestrictDeleteByDefaultConvention.cs:11-20,101): a convention-created cascade is EF Core's silent default, and an accidental one deletes a subtree nobody meant to remove.[Rubric §8, Data Architecture]assesses deliberate, reviewable delete semantics;[Rubric §4, DDD]assesses whether aggregate boundaries are respected by cascading relationships. - Walkthrough: two
[Fact]s, both driven by the one abstractModelproperty (line 31):CascadingDeletes_AreExplicitlyOptedIn(line 33) fails on a foreign key that cascades but carries no marker recording the opt-in;ConventionDeletes_AreRestricted(line 36) fails on a foreign key left at EF Core's own convention default that resolves to anything other thanRestrict. - Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs) and in MMCA.Common itself (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/DeleteBehaviorConventionTests.cs), each passing its own finalized model.
DependencyVersionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:16· Level 5 · abstract class
- What it is: a dependency-pin fitness function guarding the MassTransit v9 commercial-license trap at build time: every MassTransit package must stay below major version 9, read out of
Directory.Packages.props(ADR-016). - Depends on: ArchitectureRules
.PinnedPackageMajorBelow(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:38),[Fact]. Note there is noMapon this base; it reads the props file directly through the rule, which walks up from the test assembly's base directory to find it (FindUpwards,ArchitectureRules.Governance.cs:63). - Concept introduced, enforcing a policy pin as a test.
[Rubric §32, Dependency & Supply-Chain]assesses whether risky upgrades are guarded. The doc explains the trap: MassTransit v9 fails the startup license check and crashes every broker-enabled host, and CI never starts a broker, so a blanket bump otherwise stays green (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/DependencyVersionTestsBase.cs:4-6); the assertion's own reason records that a blanket package update reintroduced v9 once before (lines 35-38). - Walkthrough: the single
[Fact]MassTransit_MustNotExceed_MajorVersion8(line 28) loopsMassTransitPackageIds(lines 18-25:MassTransit,MassTransit.RabbitMQ,MassTransit.Azure.ServiceBus.Core,MassTransit.AmazonSQS,MassTransit.SqlTransport.PostgreSQL) and callsPinnedPackageMajorBelowwith an exclusive ceiling of 9 (line 34). The list isvirtual, so a repo that does not reference MassTransit can override it with an empty list, which makes the pin vacuous (doc, lines 7-8). The rule asserts both that the package is still pinned and that its major is below the ceiling, on the nullable major directly rather than dereferencing after a not-null assertion, a shape the comment records as avoiding a CI-only IDE0370 (ArchitectureRules.Governance.cs:40-45). - Why it's built this way: the doc is explicit (lines 8-14): the consumer repos (ADC, Store) pin only
MassTransit.Azure.ServiceBus.Core, whileMassTransitandMassTransit.RabbitMQflow transitively viaMMCA.Common.Infrastructure, so they must not subclass this base with the default list, or the "must remain pinned" assertion would fail on the pins they do not declare. The full v8 pin is enforced in MMCA.Common, where every MassTransit package is pinned; a consumer that ever needs its own guard overrides the list with the ids its ownDirectory.Packages.propsdeclares. - Where it's used: subclassed only in MMCA.Common, as the body-less DependencyVersionTests (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/DependencyVersionTests.cs:9).
DomainEventHandlerSaveTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/DomainEventHandlerSaveTestsBase.cs:22· Level 5 · abstract class
- What it is: a domain-event-handler purity gate: a handler must not persist, and the check is TRANSITIVE, walking the call graph out of every handler method rather than only looking for a save typed into the handler itself.
- Depends on: IArchitectureMap and ArchitectureRules
.DomainEventHandlersDoNotSave(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.DomainEventHandlerSaves.cs:76), which is built on CallGraphIndex. - Concept introduced, the second write inside the first one. Dispatch happens after
SaveChangesAsync(and after commit inside anITransactionalcommand), so a handler that saves opens a second write in the middle of the first: it re-enters the change tracker, can raise a fresh event cascade, and persists work the outer transaction may still roll back. Handlers mutate state and let the owning unit of work flush it; an independent write belongs on the outbox (doc, lines 3-9; ADR-003 is where the outbox itself is decided).[Rubric §6, CQRS & Event-Driven]assesses event-handling discipline;[Rubric §8, Data Architecture]assesses transaction integrity;[Rubric §7, Microservices Readiness]assesses whether a handler stays movable. - Walkthrough
- The subclass supplies
Map(line 24) and optionallyAllowedSavingTypes(line 33) andMaxCallDepth(line 40, default 6, which the doc justifies as covering the realistic handler to service to helper to repository chain while keeping the scan fast). - The default allowlist is
["MMCA.Common"](line 33), and the reason is precise: the framework's outbox event bus persists by design, and a handler publishing an integration event is not the defect this rule hunts. A handler calling a save DIRECTLY is still reported, because detection happens at the call site and needs no descent, so the default silences nothing real (lines 26-32, and the rule doc atArchitectureRules.DomainEventHandlerSaves.cs:50-60). - The single
[Fact]DomainEventHandlers_ShouldNotReach_SaveChanges(line 43) forwards all three to the rule. - The rule identifies handlers by the
IDomainEventHandlerinterface full name throughCallGraphIndex.Implements(ArchitectureRules.DomainEventHandlerSaves.cs:96), then breadth-first walks to a save with a visited set, reporting the shortest chain it finds (FindSavePath, line 125). A save is a call toSaveChangesorSaveChangesAsync(line 12) on a declaring type whose name ends withDbContext,DbContextFactory,UnitOfWorkorRepository(line 20), which is what keeps an unrelatedSaveChangesout of the rule. - The walk follows direct calls, delegate creations (so lambdas and local functions are covered), and interface or virtual calls expanded to every implementation inside the scanned assemblies, and it descends into the compiler-generated state machine of an
asyncor iterator method, whoseMoveNextholds the real body (doc, lines 44-49).
- The subclass supplies
- Why it's built this way: an allowlist entry does two things, and the doc says so explicitly (lines 15-20 of the base): it silences the type it names AND stops the walk from descending into it, which is the correct reading of "we accept what happens in there". Four limits are stated in the rule (
:61-69): the depth bound, unresolvable interface dispatch when the implementation lives outside the map, name-and-arity overload matching that over-reports rather than misses, and total blindness to reflection, DI-resolved delegates and expression trees. - Where it's used: subclassed in ADC, which accepts one deliberate cascade, the gamification points ledger written by the awarder its point-scoring handlers call (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:11, entry and reasoning at:22-27), and in Store, whose two entries are both the compensating-saga pattern, a handler opening its own DI scope for an independent, idempotent follow-up write (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/DomainEventHandlerSaveTests.cs:19, rationale at:11-17).
DomainPurityTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:8· Level 5 · abstract class
- What it is: a framework-independence base: Domain and Shared stay free of infrastructure frameworks, and Application stays host-agnostic (no EF Core, no ASP.NET Core).
- Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Purity.cs, whose sharedForbiddenDomainDependencieslist is at line 9). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds an
ExtraForbiddenDomainDependencieshook (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/DomainPurityTestsBase.cs:12) so a repo bans its own frameworks (the doc cites Store banning "Stripe" and ADC banning "RabbitMQ").[Rubric §3, Clean Architecture]and[Rubric §4, DDD]assess the framework-free core. - Walkthrough: four
[Fact]s:Domain_ShouldBe_FrameworkFree(line 15) andShared_ShouldBe_FrameworkFree(line 18), both passing the extra-forbidden list, thenApplication_ShouldNotDependOn_EntityFrameworkCore(line 21) andApplication_ShouldNotDependOn_AspNetCore(line 24), whose rules sit atArchitectureRules.Purity.cs:23,:39,:54and:68. - Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/DomainPurityTests.cs:9,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/DomainPurityTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/DomainPurityTests.cs:3, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:10).
DomainThrowTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/DomainThrowTestsBase.cs:21· Level 5 · abstract class
- What it is: a Result-pattern purity gate (ADR-013): the domain returns Result, it does not throw. Any
throwin the map's Domain assemblies whose exception is not an argument guard fails the build. - Depends on: IArchitectureMap and ArchitectureRules
.DomainThrowsOnlyArgumentGuards(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.DomainThrows.cs:71), which reads IL withMono.Cecil. - Concept introduced, why a thrown business failure is a defect and not a style choice. It skips
Result.Combineinvariant composition, arrives at the API as a 500 instead of the outcome the caller can act on, and pays an exception unwind on a path that is not exceptional (doc, lines 4-9; rule doc atArchitectureRules.DomainThrows.cs:26-32). The three argument guards stay allowed because they report a caller BUG rather than a business outcome, and a caller cannot recover from passing null, so there is nothing for aResultto carry (:11-21).[Rubric §4, DDD]and[Rubric §15, Best Practices & Code Quality]assess the error-handling model;[Rubric §9, API & Contract Design]assesses the status code a caller actually sees. - Walkthrough
- The subclass supplies
Map(line 23) and optionallyAllowedThrowingTypes(line 31, empty by default, which allows nothing beyond the argument guards). - The single
[Fact]Domain_ShouldNotThrow_ExceptArgumentGuards(line 34) forwards both to the rule. - The rule opens each Domain assembly with
ModuleDefinition.ReadModuleoverDomainAssemblyLocations(ArchitectureRules.DomainThrows.cs:111) and, for everythrowinstruction, reads the type of the exception constructed immediately before it (ConstructedExceptionType, line 143). - Three things it deliberately does not touch (doc, lines 39-48): a bare rethrow compiles to a distinct opcode and is ignored, so preserving a caught exception stays free; the
ArgumentNullException.ThrowIfNullfamily emits a plain call with nothrowin the caller, so the modern guard style always passes; and compiler-written bodies are skipped (the skeleton members of a C# extension block, and the explicit interface implementations on a compiler-generated type), because their exceptions are not anyone's code. - When the thrown value was not constructed in place (a prepared local, a field, a factory call), the type is not knowable from the instruction stream. Those sites are reported as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 49-54;
WithUnverifiablecomposes the message atArchitectureRules.ErrorCatalog.cs:152).
- The subclass supplies
- Why it's built this way: the allowlist is framed as an inventory rather than a mute button (doc, lines 14-19): adopting the rule in a repo with existing throws means converting each site to a
Result.Failureor recording why throwing is right there, which turns "the domain returns Result, mostly" into a reviewed list of every place that does not. - Where it's used: subclassed in ADC, whose allowlist is an explicit triage placeholder filled from the first run (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/DomainThrowTests.cs:9, list at:14-17), and in Store, which overrides nothing at all because the scan over its three module Domain assemblies found no non-guard throw at adoption (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/DomainThrowTests.cs:14, that decision recorded at:8-12).
EntityConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:10· Level 5 · abstract class
- What it is: the fuller DDD entity and aggregate convention base (the module-bearing counterpart to AggregateConventionTestsBase): entities are sealed and live only in Domain, aggregate roots use a
Create(...)factory returningResult<T>with no public constructor, every domain and value-object factory returns aResult, entity properties carry no public setter, and DTOs and requests stay out of Domain and Infrastructure. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Entities.cs). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase). The property-setter rule is the one worth calling out on its own: mutation goes through named domain methods rather than a setter, which is what keeps an invariant enforceable in one place (doc, lines 6-8).
[Rubric §4, DDD]and[Rubric §3, Clean Architecture]apply. - Walkthrough: eight
[Fact]s:Domain_ShouldExpose_AggregateRoots(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/EntityConventionTestsBase.cs:15),AggregateRoots_ShouldHave_ResultReturningCreateFactory(line 18),AggregateRoots_ShouldHave_NoPublicConstructors(line 21, the module-scoped rule atArchitectureRules.Entities.cs:167),DomainFactories_ShouldReturn_Result(line 24),DomainEntities_ShouldBe_Sealed(line 27),DomainEntities_ShouldReside_InDomainLayer(line 30),DomainEntityProperties_ShouldNotHave_PublicSetters(line 33, delegating toArchitectureRules.EntityPropertySettersAreNonPublic,ArchitectureRules.Entities.cs:149), andDtosAndRequests_ShouldNotResideIn_DomainOrInfrastructure(line 36). - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/EntityConventionTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/EntityConventionTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:89).
ErrorCatalogTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ErrorCatalogTestsBase.cs:20· Level 5 · abstract class
- What it is: a two-rule gate over the set of Error codes a repo's modules construct: one code means one thing (no literal code owned by two different types), and a code carries the prefix of the vocabulary that owns it.
- Depends on: IArchitectureMap and three ArchitectureRules members,
ErrorCodesAreUnique(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.ErrorCatalog.cs:63),ErrorCodesUseAnAllowedPrefix(:102) andDistinctErrorCodeCount(:135), plus AwesomeAssertions. - Concept introduced, the error code as a public vocabulary. A client switches on a code and a support ticket quotes it, so two modules that both ship the same code make that vocabulary ambiguous, and the ambiguity only surfaces in production (rule doc,
ArchitectureRules.ErrorCatalog.cs:32-38).[Rubric §9, API & Contract Design]assesses the stability and legibility of a published error surface;[Rubric §15, Best Practices & Code Quality]assesses whether a code is traceable back to the code that raised it. Uniqueness is measured per declaring TYPE, which is the subtle and correct choice: reusing one code across two branches of the same class is one error with two exits and passes, while the same code owned by two different types is the collision the rule exists to catch (doc, lines 45-50). - Walkthrough
- Four overridable members:
AllowedCodePrefixes(line 29, defaulting toMap.ModuleNames, which is the convention),AllowedSharedCodes(line 35, defaulting to the three generic statics on the framework'sErrorclass that exist precisely to be reused),MinimumErrorCodes(line 43, default 1), andIsCodePrefixAllowed(line 62), a hook for a convention a prefix list cannot express. - Three
[Fact]s:ErrorCodes_ShouldBe_Unique(line 46),ErrorCodes_ShouldCarry_TheOwningModulePrefix(line 50, passing the delegate rather than the list, so the hook is what the rule actually calls), andErrorCodeCatalog_ShouldNotBe_Empty(line 54), the non-vacuity guard that fails when the map registers no module Domain or Application assemblies. - Codes are read out of IL at the
Errorfactory call sites across the per-module Domain and Application assemblies. The recognised members are every static factory onErrorplus the primary constructor (ErrorFactoryNames,ArchitectureRules.ErrorCatalog.cs:17-30); the record's copy constructor is excluded by a first-parameter-is-string check, so awithexpression is not mistaken for a new code (comment, lines 11-15). - A code that is not a literal (built by concatenation, or read from a field) cannot be judged statically, so it is listed as UNVERIFIABLE in the failure message rather than passed or failed (doc, lines 51-55), the same honesty the domain-throw rule shows.
- Four overridable members:
- Why it's built this way: framework layers are deliberately out of scope (doc, lines 39-44), because the framework's codes are not the consumer's catalog. Scoping uniqueness to the module Domain and Application assemblies is what keeps a shared framework code from being reported as a collision in every repo.
- Where it's used: subclassed in ADC and Store, both of which namespace their codes by AGGREGATE rather than by module name, so both extend
Map.ModuleNameswith that vocabulary and treat the list as the reviewed inventory: a code under a new prefix fails until the prefix is added (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ErrorCatalogTests.cs:20, prefixes from:28;MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ErrorCatalogTests.cs:19, prefixes from:27). Both subclass docs record the same constraint: error codes are a public contract that reaches clients in problem-details payloads, so nothing already shipped is renamed to satisfy the rule, and the legitimately shared codes are frozen inAllowedSharedCodesinstead.
EventConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:10· Level 5 · abstract class
- What it is: an integration-event convention base (ADR-010): every concrete integration event inherits BaseIntegrationEvent, declares an
int SchemaVersion, carries[EventName](a stable wire identity for its outbox and inbox rows), and lives in a*.IntegrationEventsnamespace in the Shared layer. It also polices the upcasters that carry a retired contract forward. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Events.csandArchitectureRules.Upcasters.cs). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase).
[Rubric §6, CQRS & Event-Driven]and[Rubric §9, API & Contract Design]assess versioned, discoverable cross-service event contracts. It pairs with IntegrationEventContractTestsBase, which freezes the exact shape. - Walkthrough: six
[Fact]s. The four event-shape rules come first:IntegrationEvents_ShouldDeclare_SchemaVersion(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/EventConventionTestsBase.cs:15),IntegrationEvents_ShouldInherit_BaseIntegrationEvent(line 18),IntegrationEvents_ShouldDeclare_EventName(line 21, which fails any concrete event lacking[EventName]declared on the type itself, since the attribute is not inherited, so a rename or namespace move cannot orphan persisted rows),IntegrationEvents_ShouldResideIn_SharedIntegrationEventsNamespace(line 24), backed byArchitectureRules.Events.cs:6,:17,:33and:44. Two upcaster rules follow (ADR-090, doc lines 6-7):EventUpcasters_ShouldHave_UniqueSourceTypes(line 27, delegating toArchitectureRules.Upcasters.cs:14, because with two IEventUpcaster implementations reading one source contract the message a handler receives would depend on DI registration order) andEventUpcasters_ShouldIncrease_SchemaVersion(line 30, delegating toArchitectureRules.Upcasters.cs:36, which skips a source or target whoseSchemaVersionis missing or non-int, that being the first rule's business). A repo with no upcasters passes both vacuously (doc, line 7). - Where it's used: subclassed in every repo that publishes integration events: Store (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/EventConventionTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/EventConventionTests.cs:3), Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:38), and MMCA.Common itself under the nameEventVersioningConventionTests(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/EventVersioningConventionTests.cs:12).
FeatureFlagLifecycleTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FeatureFlagLifecycleTestsBase.cs:10· Level 5 · abstract class
- What it is: a governance base over FeatureFlagAttribute: every declared flag must state a
Lifetime, and aTemporaryflag must not still be in the codebase past its ownRemoveBydate. - Depends on: IArchitectureMap and
ArchitectureRules.FeatureFlagsDeclareLifetime/ArchitectureRules.TemporaryFeatureFlagsAreNotPastRemoveBy. - Concept introduced, expiring a toggle as a build-time fact.
[FeatureFlagAttribute]records aLifetime(PermanentorTemporary) and, for a temporary flag, aRemoveBydate inyyyy-MM-dd(MMCA.Common/Source/Core/MMCA.Common.Shared/FeatureFlags/FeatureFlagAttribute.cs:10-11,34-46;RemoveByFormat, line 35). A temporary toggle that is never revisited becomes permanent by neglect, carrying dead branches indefinitely; this base turns "review this flag" from a calendar reminder someone has to remember into a test that goes red on its own.[Rubric §5, Modularity & Vertical Slicing]assesses whether a codebase actively retires its own temporary branching rather than accumulating it;[Rubric §31, Configuration & Feature Management]assesses lifecycle discipline over feature toggles. - Walkthrough: the subclass supplies
Map(line 12) and can overrideToday(line 19, defaultDateOnly.FromDateTime(DateTime.UtcNow)) to freeze the judgment date, for example to a release date rather than the build clock. Two[Fact]s:FeatureFlags_ShouldDeclare_Lifetime(line 21) andTemporaryFeatureFlags_ShouldNotBe_PastRemoveBy(line 24, passingTodayso the rule judges against a fixed date rather than "now" drifting the assertion's outcome between a local run and a CI run days later). - Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FeatureFlagLifecycleTests.cs).
FolderWidthTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/FolderWidthTestsBase.cs:14· Level 5 · abstract class
- What it is: the folder-width fitness base (ADR-109): no folder under a repo's
Source/orTests/tree holds more than 12 direct code files, so a folder keeps naming a feature instead of drifting into a technical bucket (doc,FolderWidthTestsBase.cs:3-13). - Depends on: ArchitectureRules (
FoldersStayNarrow,MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.FolderWidth.cs:34) and, in every subclass, ArchitectureMapBase.FindRepoRoot. It does not take an IArchitectureMap: the defect it hunts is a layout one, so the rule walks the filesystem rather than IL (rule doc,ArchitectureRules.FolderWidth.cs:9-11). - Concept introduced, the layout rule as a test.
[Rubric §5, Modularity & Vertical Slicing]assesses whether a codebase is organised by feature rather than by technical kind. A flatServices/holding forty files is not a compiler error and no reviewer catches it in the diff that adds the forty-first file, so the convention is only enforceable if something asserts it continuously. This base is that assertion, and it is the same delegating shape as the other architecture bases (AggregateConventionTestsBase): overridable knobs, one[Fact], the judgement itself living inArchitectureRules. - Walkthrough
- Three members to override or accept:
RepoRoot(line 20, abstract, the only required one),MaxDirectFiles(line 23, virtual, default 12), andExemptFolderSuffixes(line 30, virtual, default empty). - One
[Fact],Folders_stay_narrow(line 33), which passes all three straight toArchitectureRules.FoldersStayNarrow. - The rule walks
Source/andTests/(skipping a tree that does not exist), collects every offending folder asrelative: count direct code files (max N), sorts the list ordinally and hands it toArchitectureAssert.NoViolations, so a failure reports every offender at once rather than the first (ArchitectureRules.FolderWidth.cs:42-70). - Counting is deliberate about what an author actually chose to put in a folder: a
.razorfile counts once and its co-locatedX.razor.cscode-behind counts with it,.resxsatellites never count, and generated files (*.g.cs,*.generated.cs,*.Designer.cs) never count (ArchitectureRules.FolderWidth.cs:88-126). Whole trees are skipped by path segment:bin,obj,Migrations,Platforms,Resources,node_modules,wwwroot,.git(:78).
- Three members to override or accept:
- Why it's built this way: exemptions are matched as repo-relative path SUFFIXES with forward slashes (
:85), not as leaf names, so a repo can exempt one specific folder without accidentally exempting every folder that shares its name. The alternative, a leaf-name allowlist, would silently widen with every new project that reuses the name. - Where it's used: two thin subclasses today. ADC supplies only its root and takes every default (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/FolderWidthTests.cs:10-12). MMCA.Common supplies a root plus three documented exemptions, and its doc records why each is deliberate: the decorator pipeline folder (nine cross-cutting concerns times command and query, where splitting yields nine two-file folders), its test twin, and the entity marker interfaces (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/FolderWidthTests.cs:16-30).
ForwardedJwtAudienceTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Api/ForwardedJwtAudienceTestsBase.cs:11· Level 5 · abstract class
- What it is: a source-text gate for cross-service token validation: every
AddForwardedJwtBearer(...)call in a repo'sSourcetree must resolve its audience fail-closed throughJwtAudience.RequireConfigured(...)and carry no??fallback (ADR-004). - Depends on: IArchitectureMap (only for
Map.RepoToken, which names the.slnxused to find the repo root) and ArchitectureRules.ForwardedJwtBearerAudienceIsFailClosed(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Api/ArchitectureRules.ForwardedJwt.cs:25). The runtime half isJwtAudience.RequireConfigured(MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/Auth/JwtAudience.cs:28), reading theJwt:Audiencekey (:20). - Concept introduced, scanning text because the defect compiles. The rule doc (
ArchitectureRules.ForwardedJwt.cs:10-21) names the failure: a host that readconfiguration["Jwt:Audience"] ?? "SomeApi"boots with an audience nobody configured and answers every request with a 401 that reads like a token problem. No type-level rule can see that, so the rule reads.cstext, blanks line comments (:72-74), finds each member-access call and reads its argument list to the matching close parenthesis.[Rubric §8, Security & Authorization]and[Rubric §7, Microservices Readiness]apply. - Walkthrough
- The subclass supplies
Map(line 13) andMinimumForwardedJwtHosts(line 19), normally the repo's number of extracted service hosts that are not the token issuer; the floor keeps the gate non-vacuous. SourceRoots(line 24) is virtual and defaults to the repo'sSourcefolder, located throughArchitectureMapBase.FindRepoRoot($"{Map.RepoToken}.slnx").- The single
[Fact]ForwardedJwtBearerHosts_ResolveTheirAudienceFailClosed(line 28) forwards to the rule. A call whose arguments lackJwtAudience.RequireConfigured(is a violation (:46-49), so is one containing??(:50-53), a missing root is a violation (:34-38), and finding fewer calls than the floor fails (:58-61).
- The subclass supplies
- Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Api/ForwardedJwtAudienceTests.cs:10). MMCA.Common exercises the rule through its own fitness test (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Api/ForwardedJwtAudienceFitnessTests.cs).
HandlerConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:8· Level 5 · abstract class
- What it is: the CQRS handler convention base: handlers and validators live only in Application, handlers and services do not broker other handlers, and no
*Serviceexceeds the god-class constructor-arity ceiling. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Handlers.cs). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); it adds a
MaxServiceConstructorParametersoverride (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerConventionTestsBase.cs:12, default 8, matching the rule's own default atArchitectureRules.Handlers.cs:44).[Rubric §6, CQRS & Event-Driven]and[Rubric §1, SOLID]apply; the CQRS decorator pipeline itself is taught in primer §2. - Walkthrough: six
[Fact]s:Handlers_ShouldResideIn_ApplicationLayer(line 15),Handlers_ShouldNotInject_OtherHandlers(line 18),ApplicationServices_ShouldNotInject_Handlers(line 21),ApplicationServices_ShouldNotExceed_ConstructorArity(line 24, passing the max),Validators_ShouldResideIn_ApplicationLayer(line 27),EventHandlers_ShouldResideIn_ApplicationLayer_AndBeSealed(line 30). The rules sit atArchitectureRules.Handlers.cs:6,:18,:31,:44,:70and:82. - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/HandlerConventionTests.cs:13,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerConventionTests.cs:16,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:79). ConstructorDependencyCountTestsBase is the narrower, per-repo-pinned version of the arity check.
HandlerResultConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:16· Level 5 · abstract class
- What it is: an opt-in base asserting that every concrete command or query handler's
TResultis Result orResult<T>(or a type derived from them), turning a runtime-only constraint into a build-time gate. - Depends on: IArchitectureMap and ArchitectureRules (
ApplicationLayersDeclareHandlers,CommandHandlersReturnResult,QueryHandlersReturnResult, atMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.HandlerResults.cs:18,:39,:48). - Concept introduced, closing a deliberately unconstrained generic. The CQRS interfaces carry no compile-time constraint on
TResult(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/HandlerResultConventionTestsBase.cs:6-7), but the decorator pipeline's short-circuit paths (feature gate, validation) fabricate failures through ResultFailureFactory, which throwsInvalidOperationExceptionat runtime for any non-ResultTResult(lines 7-9). A handler with the wrong result type therefore compiles cleanly and only fails when a gate short-circuits it. This base moves that failure to CI.[Rubric §6, CQRS & Event-Driven],[Rubric §14, Testability], and[Rubric §15, Best Practices & Code Quality]apply. - Walkthrough: three
[Fact]s.ApplicationLayers_DeclareAtLeastOneHandler(line 21) is the non-vacuity guard the doc calls out (lines 12-13): a mis-pinned assembly cannot make the other two pass by finding nothing.CommandHandlers_Return_ResultTypes(line 24) andQueryHandlers_Return_ResultTypes(line 27) delegate to the matching rules, which share the privateHandlersReturnResultimplementation (ArchitectureRules.HandlerResults.cs:51) and judge the result type by the two full-name constants at:9-10. - Why it's built this way: it is opt-in and map-driven like the rest of the family, so a repo adds it next to its other architecture test classes with the same
Mapand no other wiring. - Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:12), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:6), and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/HandlerResultConventionTests.cs:8).
IdempotencyConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/IdempotencyConventionTestsBase.cs:10· Level 5 · abstract class
- What it is: a one-rule delegating base asserting that every POST action in a repo's API layer states, in code, whether a retried request replays the original response (IdempotentAttribute) or deliberately does not (NonIdempotentAttribute with a written reason).
- Depends on: IArchitectureMap and ArchitectureRules
.PostActionsDeclareIdempotencyIntent(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:44). - Concept introduced, gating an omission rather than a mistake. The rule's remarks make the argument (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.Idempotency.cs:21-29): POST is the one verb HTTP does not define as idempotent, so a client retrying a timed-out POST cannot know whether the first attempt landed. The framework'sIdempotency-Keyfilter (ADR-017) costs an existing client nothing, because it no-ops for a request with no key header. The only failure mode worth gating is therefore the silent omission: an action that should replay but does not. The gate leaves the opt-out available as long as it is written down.[Rubric §9, API & Contract Design]assesses contract-level retry semantics;[Rubric §29, Resilience & Business Continuity]assesses safe retry behavior. - Walkthrough: the subclass supplies
Map(line 12); the single[Fact]PostActions_ShouldDeclare_IdempotencyIntent(line 15) forwards to the rule. In the rule,UndeclaredPostActions(line 67) enumerates public instance methods on concrete API-layer controller types, keeps those carryingHttpPostAttribute, and reports the ones carrying neitherIdempotentAttributenorNonIdempotentAttribute(lines 71-74). Attributes are matched by simple type name (HasAttributeNamed, line 80) withinherit: true, so a concrete controller inheriting a framework base action satisfies the rule through the base, and abstract controller types are skipped as declaration sites (remarks, lines 31-36). - Caveats / not-in-source: only
[HttpPost]is recognised. An action routed through[AcceptVerbs("POST")]or a conventional route is out of scope, which the remarks justify on the grounds that neither appears in this framework or its consumers (lines 38-42). - Where it's used: subclassed in all four repos, as IdempotencyConventionTests in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:10), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/IdempotencyConventionTests.cs:3), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:65, whose comment records thatTicketsController's POST actions are all opted in, so the gate is non-vacuous,:61-64).
ImmutabilityTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:8· Level 5 · abstract class
- What it is: an immutability convention base: DTOs, command and query messages, domain events, integration events, and value objects expose no public mutable (non-
init) setter; value objects are additionally sealed and confined to the Shared layer. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Immutability.cs), which uses RuleHelpers.HasPublicMutableSetterunderneath through the sharedMutablePropertyViolationsprojection (ArchitectureRules.Immutability.cs:76). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the
init-only versus mutable distinction is exactly whatHasPublicMutableSetterdetects via theIsExternalInitmodifier.[Rubric §15, Best Practices & Code Quality]and[Rubric §4, DDD]assess immutable contracts and value objects. - Walkthrough: five
[Fact]s:Dtos_ShouldBe_Immutable(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/ImmutabilityTestsBase.cs:13),CommandsAndQueries_ShouldBe_Immutable(line 16),DomainEvents_ShouldBe_Immutable(line 19),IntegrationEvents_ShouldBe_Immutable(line 22),ValueObjects_ShouldBe_ImmutableSealedAndInShared(line 25), backed byArchitectureRules.Immutability.cs:6,:18,:34,:45and:56. - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/ImmutabilityTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/ImmutabilityTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:94).
InlineStyleTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/InlineStyleTestsBase.cs:12· Level 5 · abstract class
- What it is: a design-system gate: no
.razorfile under the scanned roots carries an inline style, whether an HTMLstyle=attribute, a MudBlazorStyle=parameter,CellStyle=/HeaderStyle=, or any*StyleFunc=. - Depends on: ArchitectureRules
.RazorMarkupCarriesNoInlineStyles(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.InlineStyles.cs:37). There is noMapon this base; the subclass hands over the repo root directly. - Concept introduced, a markup-scanning gate with an allowlist meant to stay empty.
[Rubric §20, Design System & Theming]is cited by the rule doc (ArchitectureRules.InlineStyles.cs:8-15): layout and sizing belong in semantic classes in a stylesheet or scoped.razor.css, where one declaration changes a theme, and a genuinely dynamic value belongs in a class toggled from code or a CSS custom property. The matcher requires the name not to follow a word character or hyphen, so CSS text such aslist-styleanddata-stylestay out (:18-21). It shares the scanning idea with FormsConventionTestsBase. - Walkthrough
- The subclass supplies
RepoRoot(line 15, typicallyArchitectureMapBase.FindRepoRoot("MyRepo.slnx")) andMinimumRazorFiles(line 18), the floor that stops a moved root from passing vacuously. MarkupRoots(line 21) defaults toSourceunder the repo root, andAllowedInlineStyles(line 27) defaults to empty; each entry is a violation prefix relative to its markup root (Folder/File.razororFolder/File.razor:12).- The single
[Fact]RazorMarkup_CarriesNoInlineStyles(line 30) forwards to the rule, which reports a missing root (:47-51), applies the allowlist byStartsWith(:58), and fails when fewer files than the floor were scanned (:63-66).
- The subclass supplies
- Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/InlineStyleTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/InlineStyleTests.cs:10); Common also pins the detector line by line inInlineStyleFitnessTests.cs.
IntegrationEventContractTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventContractTestsBase.cs:18· Level 5 · abstract class
- What it is: a frozen wire-contract guard: it rebuilds the live integration-event contract (one line per event, a key followed by its members in braces, where the key is the event's
[EventName]value when it declares one and its full type name otherwise) and compares it to a committed snapshot the subclass supplies, so a renamed, removed, or retyped property, or a new event shipped without its consumer, fails the build. - Depends on: IArchitectureMap, ArchitectureRules
.BuildIntegrationEventContract(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:66), and ArchitectureAssert. - Concept introduced, the snapshot fitness function, and why it compares as a SET.
[Rubric §9, API & Contract Design]and[Rubric §7, Microservices Readiness]assess whether cross-service contracts stay stable; because a consumer in another service deserializes by shape, this gate makes any contract change a deliberate, coordinated commit. The member list inside each event's braces is compared as a set rather than a sequence (doc, lines 10-16), and the reasoning is worth reading: JSON carries no member order, so reordering two properties in a record's declaration changes nothing a consumer can observe, and failing the build for it would train the one gate that guards real breakage to be updated by rote. Everything observable stays a failure: a missing member, an extra member, a changed type, and any change to the set of events itself. - Walkthrough
- The subclass supplies
Map(line 20) and the committedExpectedContractsnapshot (line 33), whose lines take the shapeKey { Prop:Type, ... }(doc, lines 30-31). SnapshotOutputVariable(line 27) names the environment variableMMCA_CONTRACT_SNAPSHOT_OUT. When it is set to a file path, the fact writes the live contract there as ready-to-paste C# string literals, one per line, before comparing (lines 40-44). It only regenerates the snapshot text: the comparison still runs and still fails on drift (doc, lines 23-25).IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot(line 36) builds the actual contract (line 38) and funnels the differences through ArchitectureAssert with a message instructing the author to version the event, coordinate the consumer rollout, and updateExpectedContractin the same commit, and pointing at the environment variable as the way to get the live contract as literals (lines 46-52).Compare(line 63) parses both sides and reports three classes of difference: an event the committed contract declares and the code no longer does (line 70), a NEW event shipped without a consumer rollout or a snapshot update (line 73), and per-event member differences (line 76).CompareMembers(line 90) reports a same-named member with a different type as a retype rather than as an unrelated add and remove, because that is the failure a consumer actually sees: the property still deserializes, into the wrong shape (doc, lines 82-84). Missing and extra members are reported separately (lines 99, 111).Parse(line 123) turns contract lines into an event-to-members map, and keeps a line that does not carry the braced shape whole as an event with no members, so a malformed committed literal surfaces as a mismatch rather than being silently dropped (doc, lines 117-119). Braces are stripped from that key deliberately, because the reported difference is formatted through the shared assertion helper and a stray brace in a reason string is a format hazard (comment, lines 133-134).SplitTopLevel(line 163) splits a member list only on commas outside angle brackets, tracking the bracket depth (line 170), so a multi-argument generic member type such asIReadOnlyDictionary<String, Int32>stays one member rather than being cut in two (doc, lines 158-159);Parsecalls it for every braced line (line 142).
- The subclass supplies
- Where it's used: subclassed in the repos publishing integration events: Store (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventContractTests.cs:3), and Helpdesk, whose one-line snapshot is a compact worked example (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:99, snapshot at:105-108). It complements EventConventionTestsBase and has a synchronous counterpart in ProtoContractTestsBase.
IntegrationEventPayloadPurityTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:33· Level 5 · abstract class
- What it is: a two-rule payload-purity gate for the cross-service async API (ADR-010): an integration event type must ship from a
*.Sharedassembly, and no property on its wire shape (including a nested payload record) may reach a type declared in a*.Domainassembly. - Depends on: IArchitectureMap, ArchitectureRules
.IntegrationEventsLiveInSharedAssembliesand.IntegrationEventPayloadsAreDomainFree(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Events.cs:61). - Concept introduced, why the public contract gets its own layer rule and its own domain-leak rule. The class doc draws the line this base enforces (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:6-10): a domain event is an internal fact inside one bounded context, free to change with the model that raises it; an integration event is the opposite, another service deserializes it by shape and cannot be asked to redeploy because a producer renamed a field. The*.Shared-only rule follows from the module system's own boundary, a consuming module may reference only another module's Shared assembly, so an event declared anywhere else is a contract nobody outside the producer is allowed to reference. The domain-freedom rule closes the leak the layer check alone would miss: publishing an entity or a domain value object on the wire exports the producer's internal model, so an internal refactor becomes a breaking change for every consumer, and fields the producer never meant to share ride along (lines 19-25).[Rubric §9, API & Contract Design]assesses the published shape;[Rubric §10, Messaging & Integration]assesses the cross-service contract discipline this base exists to hold;[Rubric §7, Microservices Readiness]assesses whether that contract keeps an extraction reversible. - Walkthrough
- The subclass supplies
Map(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/IntegrationEventPayloadPurityTestsBase.cs:35). IntegrationEvents_ShouldShipFrom_SharedAssemblies(line 38) delegates to the shared-assembly rule.IntegrationEventPayloads_ShouldNotExpose_DomainTypes(line 41) delegates to the domain-freedom rule, which walks each event's public properties, including into nested payload records this repo declares, for a reference to a*.Domain-assembly type.- Both rules are vacuous for a module-less map (the class doc, lines 28-30): MMCA.Common itself is not a module, so its own integration events are governed by its public API baseline instead of this base.
- The subclass supplies
- Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs) and in ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/IntegrationEventPayloadPurityTests.cs). It complements IntegrationEventContractTestsBase, which freezes the SHAPE of an event once it already lives in a Shared assembly and stays domain-free; this base is the gate that keeps it eligible to be frozen there in the first place.
LayerDependencyTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:7· Level 5 · abstract class
- What it is: the Clean Architecture layer-flow base: fifteen
[Fact]s asserting that the map declares the expected layers at all, and that each layer references only layers below it (Domain not on Application, Infrastructure, or API; Application not on Infrastructure or API; Infrastructure not on API; Shared on nothing above it; UI only on Shared). - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Layers.cs). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); this is the runtime half of the two-gate layer enforcement described in ADR-015, the compile-time half being
MMCA.Common/Source/Build/MMCA.Common.LayerEnforcement.targets.[Rubric §3, Clean Architecture]is the whole point. - Walkthrough
- Three overridable declarations come first:
RequiredLayers(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/LayerDependencyTestsBase.cs:16, defaulting to the five core layers Shared, Domain, Application, Infrastructure, Api),RequiredModuleLayers(line 27, defaulting to the same list), andModuleRequiredLayerOverrides(line 49, empty by default). - The per-module override is the design decision worth understanding. Both the base doc (lines 29-38) and the rule doc (
ArchitectureRules.Layers.cs:96-104) argue the same point: trimming the DEFAULT list to accommodate one deliberately thin module (a hub module that is Shared plus Application plus Api and nothing else) silently stops enforcing those layers for every OTHER module in the repo, so one thin module would buy blanket permission to forget an assembly anywhere. Naming the exception keeps the default strict and puts the weakening exactly where it is true. The base doc even carries the worked override snippet (lines 40-48). - Two non-vacuity
[Fact]s guard the rest:LayerMap_DeclaresEveryExpectedLayer(line 52) andLayerMap_ModulesDeclareEveryExpectedLayer(line 55, passing the overrides through to the three-argumentModulesDeclareLayersatArchitectureRules.Layers.cs:113). Without them a map that forgot an assembly would satisfy every dependency rule by having nothing to check (ArchitectureRules.Layers.cs:64-68). - Thirteen forbidden-edge
[Fact]s follow, each a one-line delegate onto anArchitectureRules.Layers.csmethod:Domain_ShouldNotDependOn_Application(line 59) throughUi_ShouldNotDependOn_Infrastructure(line 95). The UI trio (lines 89-95) encodes the documented exception that UI depends only on Shared for Blazor WASM compatibility.
- Three overridable declarations come first:
- Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/LayerDependencyTests.cs:9,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/LayerDependencyTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/LayerDependencyTests.cs:3, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:5). NamespaceCycleTestsBase is the finer-grained companion that looks inside a layer assembly, and ModuleIsolationTestsBase the one that looks across modules.
LifetimeTokenConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LifetimeTokenConventionTestsBase.cs:13· Level 5 · abstract class
- What it is: a component-lifetime gate: no
.razoror.razor.csfile under the scan root reads_cts.Token(or_cts!.Token) directly; components read it through_cts.LifetimeToken()instead. - Depends on: ArchitectureRules
.ComponentsReadTheirTokenThroughLifetimeToken(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LifetimeTokens.cs:28), and the runtime helper inMMCA.Common/Source/Presentation/MMCA.Common.UI/Common/ComponentLifetimeExtensions.cs. - Concept introduced, closing a disposal race with a text rule. The rule doc (
ArchitectureRules.LifetimeTokens.cs:8-17) explains: readingTokenoff a disposedCancellationTokenSourcethrowsObjectDisposedException, so a load or handler that resumed after the user navigated away crashed the circuit instead of stopping.LifetimeToken()returns an already-cancelled token onceDisposehas cancelled the source. The one permitted direct read is inside such a property's own expression-bodied declaration (:67-71).[Rubric §15, Best Practices & Code Quality]and[Rubric §14, Testability]apply. - Walkthrough
- The subclass supplies
ScanRoot(line 16, typically the repo'sSource/Modules) andMinimumCodeBehindFiles(line 19), the floor on.razor.csfiles reached. ScanAllCodeFiles(line 25) is virtual and false by default; set it to also scan plain.csfiles, for component bases written as plain classes (:43-45).- The single
[Fact]Components_ReadTheirTokenOnlyThroughLifetimeToken(line 28) forwards to the rule, which fails on a missing root (:34-37) or on fewer.razor.csfiles than the floor (:48-52).
- The subclass supplies
- Where it's used: subclassed in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/ComponentConventions/LifetimeTokenConventionTests.cs:12) and ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LifetimeTokenConventionTests.cs:10).
LocalizationResourceTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:10· Level 5 · abstract class
- What it is: an opt-in translation-coverage gate (ADR-027): a repo that ships localized
.resxresources subclasses this and lists its required cultures; the build fails if any base.resxunderSource/lacks a complete, non-empty sibling for a required culture. - Depends on: ArchitectureRules
.ResourceTranslationsAreComplete(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Localization.cs:23),[Fact]. There is noMapon this base; it scansSource/directly through the rule. - Concept introduced, a coverage fitness function for i18n.
[Rubric §27, i18n]assesses translation completeness; this gate ensures a new English string can never ship without its translation (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizationResourceTestsBase.cs:3-9). - Walkthrough: the subclass supplies
RequiredCultures(line 13, for example a single Spanish entry) and optionallyMinimumBaseResources(line 21, a non-vacuity floor whose default of 0 skips the guard). The single[Fact]Translations_AreComplete_ForEveryRequiredCulture(line 24) passes both to the rule, which reads each.resxstring entry withXDocument(ArchitectureRules.Localization.cs:81) and skips build output (:75). - Why it's built this way: single-locale repos need not subclass it (the rule is vacuous for an empty list). It pairs with LocalizedTextConventionTestsBase: this gate keeps the extracted resources translated, that gate keeps literals out of markup.
- Where it's used: subclassed in MMCA.Common as LocalizationResourceTests (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizationResourceTests.cs:12) and in Store, ADC, and Helpdesk under the nameTranslationCompletenessTests(MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/TranslationCompletenessTests.cs:13,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/TranslationCompletenessTests.cs:12); Helpdesk's requires Spanish with a floor of 3, covering the two page resx pairs and the Tickets error resources (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:130, cultures at:132, floor at:135).
LocalizedTextConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:14· Level 5 · abstract class
- What it is: a localized-text convention gate (ADR-027): user-visible literals must not be hard-coded in
.razoror.razor.csunderSource/(snackbar messages, pageTitleproperties, page-title markup, breadcrumb labels) but resolve throughIStringLocalizerresources. - Depends on: IArchitectureMap, ArchitectureMapBase
.FindRepoRoot, ArchitectureRules.UserVisibleTextIsLocalized(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.LocalizedText.cs:62), which drives six source-generated regexes (:10-32). - Concept: cross-references the markup-scanning gate idea from FormsConventionTestsBase.
[Rubric §27, i18n]assesses that visible strings follow the selected language. - Walkthrough: the subclass supplies
Map(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/LocalizedTextConventionTestsBase.cs:16) and optionallyMinimumScannedFiles(line 21, default 1) andAllowedFiles(line 28, whole-file exemptions; the preferred exemption is a per-linei18n: allowcomment, per the class doc at lines 8-11).UserVisibleText_IsLocalized(line 31) resolves the repo root and delegates to the rule with theSourcedirectory, the allowlist, and the floor (lines 33-37). - Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:11,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:14,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/LocalizedTextConventionTests.cs:12, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:142, which sets a floor of 5 against the seed's 8 razor files at:147). It pairs with LocalizationResourceTestsBase.
MicroserviceExtractionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:8· Level 5 · abstract class
- What it is: a transport-boundary base for the modular-monolith to microservices path: MassTransit, gRPC, and Protobuf must never leak into Domain, Application, or Shared, so a module behaves identically in-process or extracted and the split stays reversible.
- Depends on: IArchitectureMap, ArchitectureRules (
TransportDoesNotLeakIntoCoreLayers,ArchitectureRules.Transport.cs:19, over the sharedTransportDependencieslist at:11). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the extraction invariant (application and domain code talks to abstractions, transport choices live at the edges) is the ADR-006 / ADR-007 / ADR-008 story the doc cites (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/MicroserviceExtractionTestsBase.cs:3-7).[Rubric §7, Microservices Readiness]assesses exactly this reversibility. ServiceContractPurityTestsBase guards the same boundary from the contract side. - Walkthrough: one
[Fact]CoreLayers_ShouldNotDependOn_Transport(line 13) delegating toArchitectureRules.TransportDoesNotLeakIntoCoreLayers(Map). - Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:11,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/MicroserviceExtractionTests.cs:3, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:111).
ModuleIsolationTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:8· Level 5 · abstract class
- What it is: a modular-monolith boundary base: a module must not reach another module's internal layers; cross-module communication goes only through the Shared (contract) layer. It is vacuous for single-module or module-less repos.
- Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Modules.cs), which usesOtherModuleNamespacesto compute the forbidden targets through the privateModuleLayerIsolated(:98). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase).
[Rubric §5, Vertical Slice]and[Rubric §7, Microservices Readiness]assess module autonomy. The IModule system is taught in Group 14. - Walkthrough: seven
[Fact]s. Six name a specific pair worth its own failure message:ModuleDomains_ShouldBe_Isolated(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ModuleIsolationTestsBase.cs:13),ModuleApplications_ShouldBe_Isolated(line 16),ModuleInfrastructures_ShouldBe_Isolated(line 19),ModuleApis_ShouldBe_Isolated(line 22), plus the two cross-layer reach rulesModuleDomains_ShouldNotReach_OtherModuleInfrastructures(line 25) andModuleApplications_ShouldNotReach_OtherModuleInfrastructures(line 28).- The seventh,
ModuleInternalLayers_ShouldNotReach_OtherModuleInternalLayers(line 36), closes the coverage the other six leave open: it runs the complete cross product of six source layers (Domain, Application, Infrastructure, Api, Shared and Ui) against every other module's four internal layers, Domain, Application, Infrastructure and Api (ArchitectureRules.Modules.cs:52, the loop at:54-63). The rule doc names the gap it fixes precisely (:31-41): a per-module layer rule only forbids the SAME module's higher layers, and the compile-time layer guard only knows about framework references, so a cross-module project reference such as one module's Domain onto another's Application passed every gate while compile-coupling two modules that ADR-007/008 promise can be extracted separately. - Shared and Ui are covered as SOURCES only, not as targets (
:43-49): the per-layer Shared and Ui rules forbid only the same module's internal layers, so a module's Shared or Ui reaching another module's Domain, Application, Infrastructure or Api was unchecked, while a module's UI composing another module's UI, and any layer consuming another module's Shared contracts, are real, intended arrangements in the shipped apps (Shared to Shared has its own rule,ModuleSharedAreIsolated).
- The seventh,
- Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/ModuleIsolationTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ModuleIsolationTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:15, where the single-module seed makes it deliberately vacuous but future-proof).
NamespaceCycleTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:15· Level 5 · abstract class
- What it is: an acyclicity gate one level below the layer rules: the top-level namespaces inside each layer assembly a map declares must form a directed acyclic graph.
- Depends on: IArchitectureMap and ArchitectureRules
.NamespacesHaveNoDependencyCycles(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.Cycles.cs:45). - Concept introduced, the namespace cycle as an early extraction warning. The doc frames it plainly (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/NamespaceCycleTestsBase.cs:4-8): a namespace cycle is the first visible symptom of a folder pair that has grown into one tangled unit, where neither half can be read, tested, or lifted into its own service without the other. That is the granularity the two coarse rules (LayerDependencyTestsBase and ModuleIsolationTestsBase) cannot see inside of.[Rubric §7, Microservices Readiness]assesses extractability;[Rubric §34, Architecture Governance & Documentation]assesses coupling that makes a folder unreadable on its own. - Walkthrough
- The subclass supplies
Map(line 18) and optionallyAllowedCycleNamespaces(line 26), fully-qualified nodes whose cycle is accepted by design. - The allowance rule is the interesting part: a cycle is skipped only when every namespace on its reported path appears in the list, so an allowance can never hide a NEW cycle that merely touches an accepted namespace (doc, lines 20-25). The rule enforces that over the whole strongly connected component, not just the rendered shortest path.
- The single
[Fact]Namespaces_ShouldNotHave_DependencyCycles(line 29) forwards both to the rule, which builds a per-assembly namespace graph from base types, interfaces, field, property, method return and parameter types, and attribute types, with generic arguments and array or by-ref element types expanded (ArchitectureRules.Cycles.cs:84,:163,:174,:215,:228), then finds the strongly connected components (:253) and renders the shortest cycle through each (:330).
- The subclass supplies
- Caveats / not-in-source: the rule is signature-level reflection and blind to method bodies, because this rule carries no IL dependency, so a green result means "no STRUCTURAL cycle", not "no coupling" (doc, lines 9-13). Compiler-generated types are skipped deliberately so the answer stays a signature-level one.
- Where it's used: subclassed today only in MMCA.Common, as NamespaceCycleTests (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/NamespaceCycleTests.cs:9). ItsAllowedCycleNamespacesrecords the single accepted tangle in the framework,MMCA.Common.InfrastructuretoSettingstoPersistenceand back (:39-44), with each of the three edges justified in the doc comment above it (:13-38).
NamingConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:8· Level 5 · abstract class
- What it is: a naming and sealing convention base across the CQRS plus DDD building blocks: handlers, command and query messages, validators, DTOs, domain events, invariants, EF configurations, specifications, and repositories each follow their established suffix and sealing convention.
- Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Naming.cs), which uses RuleHelpers.SimpleNameto match suffixes on generic types and funnels the two message rules through the sharedAssertMessageSuffix(:130). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase).
[Rubric §15, Best Practices & Code Quality]and[Rubric §15, Best Practices & Code Quality]assess consistent, discoverable naming. - Walkthrough: ten
[Fact]s:Handlers_ShouldBeSealed_WithHandlerSuffix(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/NamingConventionTestsBase.cs:13),Commands_ShouldHave_CommandOrRequestSuffix(line 16),Queries_ShouldHave_QuerySuffix(line 19),Validators_ShouldHave_ValidatorOrRulesSuffix(line 22),SharedDtos_ShouldHave_DtoOrLookupSuffix(line 25),DomainEvents_ShouldBeSealed_InDomainEventsNamespace(line 28),InvariantClasses_ShouldBe_Static(line 31),EfConfigurations_ShouldBeSealed_WithConfigurationSuffix(line 34),Specifications_ShouldBeSealed_WithSpecificationSuffix(line 37),Repositories_ShouldHave_RepositorySuffix(line 40), backed by the ten rules atArchitectureRules.Naming.cs:21,:33,:37,:41,:54,:67,:79,:91,:103and:116. - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Governance/NamingConventionTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/NamingConventionTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:74).
PiiConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:19· Level 5 · abstract class
- What it is: a GDPR/CCPA right-to-erasure base (ADR-005) that checks both directions. Forward: any domain entity that declares a PiiAttribute-marked property must implement IAnonymizable, so it has an erasure path. Reverse: whatever
Anonymize()actually overwrites must carry[Pii], checked behaviorally over a per-repo sample table, plus a coverage fact that everyIAnonymizablemodule entity has a sample. - Depends on: IArchitectureMap, ArchitectureRules
.EntitiesWithPiiImplementAnonymizable(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Governance.cs:11). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the
[Pii]plusIAnonymizablesoft-delete-versus-erasure model is taught in Group 02 (ADR-005).[Rubric §30, Compliance / Privacy / Data Governance]and[Rubric §11, Security]assess erasure discipline. TheIAnonymizablecontract is matched by its full name (ArchitectureRules.Governance.cs:7) so a same-named local interface cannot satisfy the rule (comment, lines 5-6), while the[Pii]marker is matched by simple name (:50). - Walkthrough
- The optional virtual
AnonymizableSamples(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/PiiConventionTestsBase.cs:31) maps eachIAnonymizableentity type to a factory that builds it through its own domain factory, populated so every erasable field starts at a value the erasure has to move. It is typedobjectbecause this package takes no framework reference, and it defaults to an empty table (NoSamples, line 21), so a repo with noIAnonymizablemodule entity passes both reverse facts unchanged. EntitiesWithPiiProperties_ShouldImplement_IAnonymizable(line 34) delegates to the forward rule, which scans every Domain-layer type for a[Pii]-marked public instance property (HasPiiProperty,ArchitectureRules.Governance.cs:48) and reports the ones that do not implement the contract.EveryAnonymizableDomainEntity_IsCoveredBySample(line 37) feeds the types ofMap.ModuleDomain()and the sample table toArchitectureRules.AnonymizableEntitiesAreCoveredBySamples(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Anonymization.cs:12). Only MODULE Domain assemblies are scanned (class remarks): the framework's own anonymizable types are checked once, in MMCA.Common's own subclass, rather than re-sampled in every consumer.PropertiesOverwrittenByAnonymize_MustCarryPii(line 43) callsArchitectureRules.PropertiesOverwrittenByAnonymizeCarryPii(ArchitectureRules.Anonymization.cs:49) over the samples, the reverse direction: a field that erasure overwrites but that is not marked[Pii]is invisible to the forward rule and the personal-data inventory.
- The optional virtual
- Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/PiiConventionTests.cs,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Privacy/PiiConventionTests.cs:25,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiConventionTests.cs, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:116). The rules are also exercised byMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/AnonymizationRulesTests.cs(AnonymizationRulesTests).
PiiInventoryTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:33· Level 5 · abstract class
- What it is: a personal-data inventory base that keeps a repo's published
PRIVACY.mdand its[Pii]-marked code in step, in both directions. The subclass names the assemblies and the policy path; the base reads the file and runs two rules. - Depends on: PiiAttribute as the marker the rules look for, and ArchitectureRules
.PiiMembersHaveInventoryRows(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.PiiInventory.cs:23) and.PiiInventoryRowsMapToPiiMembers(:61). There is noMap: the inputs are an assembly list and Markdown text. - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase) and the companion PiiConventionTestsBase. The policy holds an inventory table whose header carries
Member | Purpose | Lawful basis | Retention, one row perType.Property(ArchitectureRules.PiiInventory.cs:6,:12,:49-50). Code to document: every[Pii]property needs a row with the three cells filled, so personal data is never held without a stated reason, basis or end date. Document to code: every row must name a live[Pii]member, so an orphan row (renamed, removed or quietly unmarked) cannot keep promising something the code no longer marks.[Rubric §30, Compliance / Privacy / Data Governance]assesses this. - Walkthrough
- Two abstract members:
Assemblies(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Privacy/PiiInventoryTestsBase.cs:36) andPrivacyPolicyPath(line 39, the absolute path ofPRIVACY.md). PiiMembers_ShouldHave_CompleteInventoryRows(line 42) forwards to the code-to-document rule andInventoryRows_ShouldMapTo_LivePiiMembers(line 46) to the document-to-code rule.- The private
ReadPrivacyPolicy(line 49) asserts the file exists, with the path in the failure message, before reading it, so a moved policy fails loudly instead of passing on empty text.
- Two abstract members:
- Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Privacy/PiiInventoryTests.cs) and exercised against fixtures inMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Privacy/PiiInventoryTestsBaseTests.cs(PiiInventoryTestsBaseTests).
ProtoContractTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:19· Level 5 · abstract class
- What it is: the frozen wire-contract guard for a repo's gRPC
.protofiles, the synchronous counterpart to IntegrationEventContractTestsBase. The subclass names its solution file, its proto files, and the committed snapshot; the base rebuilds the live contract and reports the diff. - Depends on: ArchitectureRules
.ProtoContractsMatchFrozenList(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Protos.cs:38), which resolves paths through ArchitectureMapBase.FindRepoRoot(line 45) and parses through ProtoScope / ProtoScopeKind. There is noMapon this base: it reads committed.prototext, not assemblies. - Concept introduced, pinning a contract by what actually crosses the wire. The rule's remarks are explicit about the two halves of the decision (
ArchitectureRules.Protos.cs:20-36). Pinned: the file'spackage, every service with each rpc (name, request type, response type, and both streaming flags), every message field (name, declared type, label, and field NUMBER), and every enum value and number, with nested messages and enums under their qualified name. Deliberately not pinned:syntax,importlines, andoptiondeclarations includingcsharp_namespace, because none of them changes a byte on the wire; including them would fail the gate on edits that break nobody, which is the fastest way to teach a team to update the snapshot without reading it.[Rubric §9, API & Contract Design]and[Rubric §7, Microservices Readiness]assess contract stability across a service boundary. - Walkthrough
- Three abstract members:
SolutionFileName(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Contracts/ProtoContractTestsBase.cs:22, marking the repo root),ProtoFiles(line 25, repo-root-relative paths), andFrozenProtoContracts(line 30, the committed snapshot). - The single
[Fact]ProtoContracts_ShouldMatch_TheFrozenSnapshot(line 33) forwards all three to the rule. - The comparison is two-directional:
AssertProtoContract(ArchitectureRules.Protos.cs:59) reports lines present in the protos but not frozen with a plus marker and frozen lines missing from the protos with a minus marker (lines 67-70), then funnels both through ArchitectureAssert. A.protopath that does not exist yields one explicit missing-file line rather than silently contributing nothing (line 96), and the whole snapshot is distinct-and-ordered so the comparison is stable (line 103).
- Three abstract members:
- Why it's built this way: the remarks say the snapshot is meant to be regenerated deliberately by printing
ArchitectureRules.BuildProtoContract(...)for the same files, as part of the commit that changes the contract, never edited to make a red test go green (Bases/ProtoContractTestsBase.cs:12-17). MMCA.Common ships no.protoof its own (it supplies the gRPC plumbing, not the contracts), so the framework does NOT subclass this (lines 9-11). - Where it's used: subclassed in the two repos with
*.Contractsprojects: ADC, pinning seven protos across four Contracts projects (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Contracts/ProtoContractTests.cs:3, file list at:11-17), and Store, pinning three (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Contracts/ProtoContractTests.cs:9, file list at:15-17). MMCA.Common exercises the underlying rule instead, from fixture protos including a deliberately drifted copy, in ProtoContractFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Contracts/ProtoContractFitnessTests.cs:14).
QueryHandlerReadRepositoryTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15· Level 5 · abstract class
- What it is: a one-rule delegating base asserting that a query handler asks for the narrow read surface: every
IQueryHandler<,>implementation in the map's Application assemblies must callIUnitOfWork.GetReadRepository, neverIUnitOfWork.GetRepository(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Cqrs/ArchitectureRules.QueryHandlerRepositories.cs:11-16). - Depends on: IArchitectureMap, ArchitectureRules
.QueryHandlersUseReadRepositories(ArchitectureRules.QueryHandlerRepositories.cs:37), which walks the map's Application assemblies through CallGraphIndex. - Concept introduced, an IL-level call-site rule rather than a signature rule. A read handler calling into the write repository is invisible to reflection, a call site only exists in the method body, so the rule reads IL through the Mono.Cecil NetArchTest already carries and matches the callee by name and declaring type full name against the constant
IUnitOfWorkfull name andGetRepositorymethod name (ArchitectureRules.QueryHandlerRepositories.cs:7-8,:20-24). The write repository exposes the mutation surface (add, remove, tracked reads) and exists only for aggregate roots, so a query reading a child entity cannot use it at all; the rule closes that misuse at the call site instead of trusting convention.[Rubric §4, DDD]and[Rubric §15, Best Practices & Code Quality]assess this narrow-interface discipline. - Walkthrough
- The subclass supplies
Map(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/QueryHandlerReadRepositoryTestsBase.cs:15) and optionallyAllowedHandlers(line 20, empty by default, an adoption ratchet for existing offenders by type full name or namespace prefix). - The single
[Fact]QueryHandlers_ShouldUseReadRepositories(line 26) delegates to the rule. - In the rule, every method of each
IQueryHandlerimplementation is read together with its compiler-generated nested types, which is where an async state machine or a lambda display class actually lives (ArchitectureRules.QueryHandlerRepositories.cs:20-24); a call made in a helper type the handler delegates to is outside the handler and is not reported (lines 27-29). An abstract handler base is scanned like a concrete handler, so a base that reaches for the write repository is reported once, on the base (line 29). A non-vacuity guard fails if the map's Application assemblies contain noIQueryHandlerimplementation at all (lines 59-60).
- The subclass supplies
- Where it's used: subclassed in ADC with an empty allowlist (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/QueryHandlerReadRepositoryTests.cs:9-11, adopted because every query handler already reads through the narrow interfaces) and in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/Repositories/QueryHandlerReadRepositoryTests.cs). It complements the delegating-base shape shared with AggregateConventionTestsBase and ArchitectureRules, and pairs conceptually with NamingConventionTestsBase'sRepositories_ShouldHave_RepositorySuffixfact in enforcing repository conventions from two different angles.
ServiceContractPurityTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:20· Level 5 · abstract class
- What it is: a one-rule delegating base asserting that every type marked with the framework's ServiceContractAttribute stays free of the producing service's Domain, Application, and Infrastructure, so a consumer can take the contract package without taking the producer's internals.
- Depends on: IArchitectureMap and ArchitectureRules
.ServiceContractsDoNotDependOnServiceInternals(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Contracts/ArchitectureRules.Contracts.cs:32). - Concept introduced, the attribute-driven ratchet. Two design choices are worth reading closely, and both are recorded in the base's remarks (
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/ServiceContractPurityTestsBase.cs:8-18). First, the rule is attribute-driven rather than Layer.Contracts-driven, because no repo registers that layer in its map today, so a layer-iterating rule would pass vacuously forever; scanning every registered assembly for the marker enforces the invariant wherever the contract types live. Second, the base is honest about the vacuous case: a repo that has marked no type yet asserts nothing, and the value is the ratchet, the invariant bites from the first marked type onward with no test left to remember.[Rubric §7, Microservices Readiness]assesses whether an extraction stays reversible; a contract that leaks a domain entity, a handler abstraction, or a persistence type forces every consumer to depend on the producer's internals, which is what makes an extraction irreversible (ArchitectureRules.Contracts.cs:13-19).[Rubric §9, API & Contract Design]assesses the published surface itself (ADR-007). - Walkthrough
- The subclass supplies
Map(line 22); the single[Fact]ServiceContracts_ShouldNotDependOn_ServiceInternals(line 25) forwards to the rule. - In the rule,
ServiceInternalNamespaces(ArchitectureRules.Contracts.cs:120) collects the distinct, ordered root namespaces of every Domain, Application and Infrastructure ref in the map (lines 122-128) and the rule returns immediately when that set is empty (lines 34-38). - It then loops
map.Layersand runs NetArchTest per assembly withMeetCustomRule(CarriesServiceContractAttribute)as the selector (lines 40-47). That predicate (line 132) readsMono.Cecil.TypeDefinitioncustom attributes and matches the public constantServiceContractAttributeFullName(line 10) by string, the same zero-reference stance the rest of the library takes; a reflection twin for the loaded-type rules sits at line 143. - Because the rule scans every registered assembly, a marked type that lives inside a Domain, Application or Infrastructure assembly fails by construction, and the remarks say that is the intent: a published contract belongs in a
*.Contractsor Shared assembly (ArchitectureRules.Contracts.cs:25-29).
- The subclass supplies
- Where it's used: subclassed once per repo, in all four: MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Layering/ServiceContractPurityTests.cs:11), ADC (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9), Store (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/ServiceContractPurityTests.cs:9), and Helpdesk (MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ServiceContractPurityTests.cs:9); see ServiceContractPurityTests. The ratchet has now engaged in the two multi-module apps: ADC marks five contract interfaces (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/Events/IEventLiveValidationService.cs, itsSessions/ISessionBookmarkValidationService.cssibling,MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs,MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/Exports/IUserEngagementExportService.cs,MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/Users/IAttendeeQueryService.cs, plusMMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.Shared/UserNotifications/IUserNotificationExportService.cs) and Store marks three (MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/Products/IProductVariantService.cs,MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Shared/Customers/ICustomerService.cs,MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Shared/Exports/IUserSalesExportService.cs), all of them in a module's Shared layer, which is exactly where the rule says a contract belongs. MMCA.Common and Helpdesk mark none, so their runs remain ratchets (MMCA.Common/Source/Core/MMCA.Common.Shared/Abstractions/ServiceContractAttribute.cs:12-14). It complements, and does not replace, ContractImplementationTestsBase, the transport-purity rule behind MicroserviceExtractionTestsBase, and the layer-purity rules behind LayerDependencyTestsBase (ADR-015).
SharedLayerTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:7· Level 5 · abstract class
- What it is: a Shared (contract) layer base: a module's Shared is contracts-only, so it must not depend on its own internal layers, on another module's Shared, or on EF Core.
- Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Modules.cs:67,:80,:84). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase).
[Rubric §3, Clean Architecture]and[Rubric §5, Vertical Slice]assess a clean contract boundary a would-be extracted consumer can reference safely. - Walkthrough: three
[Fact]s:ModuleShared_ShouldNotDependOn_OwnInternalLayers(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Layering/SharedLayerTestsBase.cs:12),ModuleShared_ShouldBe_Isolated(line 15),ModuleShared_ShouldNotDependOn_EntityFrameworkCore(line 18). - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Layering/SharedLayerTests.cs:3,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Layering/SharedLayerTests.cs:3,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:20).
SliceCohesionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:10· Level 5 · abstract class
- What it is: a vertical-slice cohesion base: a use-case slice keeps its command or query, its handler, and its validator together in one namespace, so a feature is a cohesive unit rather than spread across horizontal
Handlers/andValidators/folders. - Depends on: IArchitectureMap, ArchitectureRules (
ArchitectureRules.Slices.cs:40,:55, with the namespace comparison at:103). - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase).
[Rubric §5, Vertical Slice]assesses feature cohesion. The doc notes MMCA.Common scopes to its Notifications slices while ADC and Store scope to their module Application layers (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Cqrs/SliceCohesionTestsBase.cs:6-8). - Walkthrough: two
[Fact]s:Handlers_ShouldBeCoLocatedWith_TheirContracts(line 15) andValidators_ShouldBeCoLocatedWith_TheirContracts(line 19). Both rules resolve the type a handler or validator is written for (HandlerContract,ArchitectureRules.Slices.cs:88;ValidatedType,:85) and only compare when the contract is a concrete type in the same assembly (:100), so a handler written against a framework abstraction is out of scope rather than misreported. - Where it's used: subclassed in all four repos (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Cqrs/SliceCohesionTests.cs:10,MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Cqrs/SliceCohesionTests.cs:9,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Cqrs/SliceCohesionTests.cs:8, and Helpdesk atMMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:121).
SoftDeleteEnforcementTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SoftDeleteEnforcementTestsBase.cs:17· Level 5 · abstract class
- What it is: a soft-delete enforcement gate: EF Core's row-erasing members may only be called from the purge and erasure types a repo names in an allowlist. Everything else deletes by setting
IsDeleted = true. - Depends on: IArchitectureMap and ArchitectureRules
.HardDeletesOnlyInAllowedTypes(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.SoftDelete.cs:62), which reads IL withMono.Cecil. - Concept introduced, enforcing the framework's deletion model at the call site. Soft delete is the default taught in the primer, and what it buys is that a deleted row survives for audit, restore and erasure accounting; a hard delete bypasses all of it (rule doc,
ArchitectureRules.SoftDelete.cs:26-35).[Rubric §8, Data Architecture]assesses the deletion model;[Rubric §30, Compliance / Privacy / Data Governance]assesses erasure accounting (ADR-005);[Rubric §13, Observability & Operability]applies because an audit trail with a silently removed row is not an audit trail. - Walkthrough
- The subclass supplies
Map(line 19) andAllowedHardDeleteTypes(line 27, empty by default, which bans hard deletes outright). The single[Fact]HardDeletes_ShouldOnlyOccurIn_AllowedPurgeTypes(line 30) forwards both. - The scope is precise. Four member names are watched (
HardDeleteMemberNames,ArchitectureRules.SoftDelete.cs:10-11), and the two common ones (Remove,RemoveRange) are additionally scoped to four EF entity-set declaring types (EntitySetTypeNames,:18-24), which is what keeps every unrelatedRemoveon a dictionary, a list or a cache out of the rule. - An allowlist entry is a type full name or a namespace prefix, and it also covers the compiler-generated async state machines and closures nested inside the type it names (doc,
:44-49). Matching is ordinal and case-sensitive. - The stated limit (
:50-55) is the one worth remembering: the rule sees only direct calls compiled into the scanned assemblies, so a hard delete reached through an interface the repo owns is caught at the implementing type, not at the caller. That implementation belongs on the allowlist and the abstraction stays free to be used.
- The subclass supplies
- Why it's built this way: the doc frames the allowlist exactly as the cascade and domain-throw rules frame theirs (lines 10-15): adoption means moving each reported type into a fix or into the list with a comment, which turns "we soft-delete, mostly" into a reviewed inventory of every place that does not.
- Where it's used: subclassed in all three repos that carry persistence plus the framework. MMCA.Common runs the rule back on its own author, naming four framework erasers individually rather than exempting a namespace, so a fifth would still fail and get reviewed: the repository's set-based delete escape hatch, outbox and inbox retention, audit-trail retention, and refresh-session retention (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:19, entries and per-entry reasoning at:24-47). Store allowlists the same four framework types plus its own image-blob side table (MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15, rationale at:8-13), and ADC subclasses it the same way (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SoftDeleteEnforcementTests.cs:15). The framework subclass is what keeps the consumers' lists honest, since a new framework eraser would otherwise land in the packages and fail downstream instead of in the repo that wrote it.
SortableColumnConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/SortableColumnConventionTestsBase.cs:16· Level 5 · abstract class
- What it is: a MudDataGrid sorting gate: a column marked sortable must be a
PropertyColumn, never aTemplateColumn. - Depends on: ArchitectureRules
.SortableGridColumnsUsePropertyColumn(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Ui/ArchitectureRules.Markup.cs:42),[Fact]. There is noMap: the subclass supplies absolute directory roots directly. - Concept introduced, gating a defect that compiles and renders. Server-side sort reads the bound property off the column, and a
TemplateColumnhas none, so marking one sortable renders a header that toggles an arrow without ordering the data (doc, lines 4-8). The base doc says plainly why this belongs in a fitness function rather than a review: the defect compiles, renders, and is invisible until someone checks the order.[Rubric §24, Forms / Validation / UX Safety]assesses whether an interactive control does what it appears to do;[Rubric §18, UI Architecture]assesses using the right grid primitive for the job. - Walkthrough
- The subclass supplies
MarkupRoots(line 24), absolute directory paths scanned recursively for*.razor. The doc tells the author to build them from ArchitectureMapBase.FindRepoRootso the scan is independent of the runner's working directory (lines 18-23). - The single
[Fact]SortableColumns_ShouldNotBe_TemplateColumns(line 27) forwards to the rule. - The rule scans
.razorTEXT rather than IL, because the defect lives in markup that compiles perfectly (rule doc,ArchitectureRules.Markup.cs:16-19). It reads eachTemplateColumntag to its closing bracket through quoted attribute values (TagEnd,:142), so attribute order, wrapped lines and a generic type argument are all handled, and it accepts the quoted, unquoted and expression-prefixed spellings of a trueSortablevalue while leaving a value bound to a field or property alone, since that value is not knowable from the markup (HasSortableTrue,:171;IsBoundToTrue,:198). - Commented-out markup is blanked before the scan, with spaces replacing the comment and newlines surviving (
BlankRazorComments,:82), so a commented example neither fails the gate nor shifts the reported line numbers (doc,:31-35). - A missing root is reported as a violation rather than silently skipped (line 52), so a path typo cannot make the gate vacuous.
- The subclass supplies
- Where it's used: subclassed in ADC and Store, both pointing at the whole
Sourcetree so the scan covers the three module UI projects and both web heads, and both freezing an already-clean state (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11, root at:16;MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/SortableColumnConventionTests.cs:11, root at:16).
SpecificationConventionTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:10· Level 5 · abstract class
- What it is: an opt-in base for the Specification pattern in polyglot / database-per-service repos: it guarantees no specification filters by navigating to another entity, which would not translate when that entity lives in a different physical source.
- Depends on: IArchitectureMap, ArchitectureRules
.SpecificationsDoNotNavigateToOtherEntities(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.Specifications.cs:24), backed by CrossEntityNavigationFinder. - Concept: cross-references the delegating-base shape (AggregateConventionTestsBase); the Specification<TEntity, TIdentifierType> pattern is taught in Group 03.
[Rubric §8, Data Architecture]assesses engine-portable query design. - Walkthrough: one
[Fact]Specifications_ShouldNotNavigate_ToOtherEntities(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/SpecificationConventionTestsBase.cs:16) delegating to the rule, which discovers specifications by base-type full-name prefix across the Application and Domain layers (ArchitectureRules.Specifications.cs:30-33) and resolves each one's entity type from the generic base (SpecificationEntityType,:79). The doc notes single-engine repos need not subclass it (lines 4-8). - Where it's used: subclassed in Store, ADC, and Helpdesk (
MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Domain/SpecificationConventionTests.cs:7,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/SpecificationConventionTests.cs:8,MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/ArchitectureTests.cs:29). MMCA.Common exercises the same rule from the other side, through SpecificationFitnessTests (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/SpecificationFitnessTests.cs:13) and its private SpecTestMap fixture (:40).
StronglyTypedIdTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:9· Level 5 · abstract class
- What it is: an opt-in fitness base for the strongly typed identifier pattern (ADR-115): it guarantees every
IStronglyTypedIdwrapper in the repo is areadonly record structwhose only instance state is the wrappedValue, the shape the framework's EF value converter, JSON converter, and comparer are written against. - Depends on: IArchitectureMap, ArchitectureRules
.StronglyTypedIdsAreReadonlyRecordStructs(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Domain/ArchitectureRules.StronglyTypedIds.cs:34). - Concept: sibling to the other opt-in
*TestsBasefixtures (compare SpecificationConventionTestsBase above); strongly typed identifiers are the alternative to the alias-based{Entity}IdentifierTypeglobals, on the same opt-in footing smart enumerations sit on. - Walkthrough: one
[Fact]StronglyTypedIds_ShouldBe_ReadonlyRecordStructs(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Domain/StronglyTypedIdTestsBase.cs:15) delegating to the rule, which scans every distinct layer assembly on the map for types implementing the openIStronglyTypedIdinterface and asserts each is a struct, isreadonly, is arecord, and declares no state beyondValue(ArchitectureRules.StronglyTypedIds.cs:38-59). - Where it's used: subclassed by StronglyTypedIdConventionTests in MMCA.Common (
MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Domain/StronglyTypedIdConventionTests.cs) and by its ADC counterpart (MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Domain/StronglyTypedIdTests.cs).
SubstitutabilityTestsBase
MMCA.Common.Testing.Architecture ·
MMCA.Common.Testing.Architecture·MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:21· Level 5 · abstract class
- What it is: a Liskov substitutability gate: a member that overrides a base-class virtual or abstract member, or implements an interface member, must not throw
NotSupportedExceptionorNotImplementedException. - Depends on: IArchitectureMap and ArchitectureRules
.SubstitutableMembersDoNotRefuseTheContract(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Governance/ArchitectureRules.Substitutability.cs:50), which reads IL withMono.Cecil. - Concept introduced, gating a refusal that only surfaces at run time. A caller holding the base type or the interface was promised the operation; a subtype that refuses it breaks every caller that substitutes it in, on the path nobody tested with that subtype (rule doc,
ArchitectureRules.Substitutability.cs:17-23). Reflection decides which members are substitutable (an override has its base definition on another type; an implementation is a target of the type's interface map), and only an exception constructed in place and thrown is judged. A method that is neither may throw what it likes, compiler-written bodies are skipped, and anasyncoverride is judged on its own body (:25-37).[Rubric §15, Best Practices & Code Quality]assesses contract honesty. - Walkthrough
- The subclass supplies
Map(MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Governance/SubstitutabilityTestsBase.cs:23) and may overrideExcludedMembers(line 31, empty by default). An entry isNamespace.Type.Method, a nested type uses the reflectionOuter+Innerspelling and a property accessor itsget_/set_name, for a refusal that is the deliberate, reviewed choice such as a write-only stream'sRead. - The single
[Fact]OverridesAndInterfaceImplementations_ShouldNotThrow_NotSupportedOrNotImplemented(line 34) forwards both to the rule. - Scope follows the map: a module-bearing (consumer) map scans only its module assemblies so the framework is not re-judged in every consumer, while the framework's own module-less map scans every layer (
ArchitectureRules.Substitutability.cs:39-42).
- The subclass supplies
- Where it's used: subclassed in ADC (
MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Governance/SubstitutabilityTests.cs) and in MMCA.Common (MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Governance/Substitutability/CommonSubstitutabilityTests.cs, CommonSubstitutabilityTests). The rule itself is exercised against fixtures inSubstitutabilityRuleTests.csin the same folder (SubstitutabilityRuleTests).
AccessibilityViolationException
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7· Level 0 · sealed class
- What it is: the exception thrown when an axe-core accessibility scan finds one or more WCAG violations on the page under test.
- Depends on:
System.Exception(BCL) only. Its XML doc names the single thrower, PageExtensions.AssertNoAccessibilityViolationsAsync(MMCA.Common.Testing.E2E/Infrastructure/AccessibilityViolationException.cs:3-6). - Concept introduced, the accessibility gate as a hard failure. Rather than logging a warning or returning a result object, a violated a11y scan throws, so a consumer E2E
[Fact]that callsAssertNoAccessibilityViolationsAsyncgoes red and names the offending elements.[Rubric §21, Accessibility]assesses whether accessibility is verified rather than assumed; a dedicated exception type makes an a11y regression a first-class, catchable build failure.[Rubric §28, Front-End Testing]assesses whether the UI is exercised through realistic automated checks; this is the failure primitive those checks throw. - Walkthrough: three constructors, the parameterless, message, and message-plus-inner overloads (
AccessibilityViolationException.cs:10,:15,:21), the standard exception shape. It carries no extra state: the human-readable violation summary is baked into themessagestring the thrower builds. - Why it's built this way: a purpose-named exception (not a bare
ExceptionorInvalidOperationException) lets a test that deliberately probes a known-inaccessible page assert on exactly this type, and it reads clearly in a failure log. - Where it's used: thrown only by PageExtensions
.AssertNoAccessibilityViolationsAsync(MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:355-356), which is in turn called by theScanAsync/ScanGridAsynchelpers on E2ETestBase and by the*_ShouldHaveNoAccessibilityViolationsfacts on the workflow bases.
AdminCredentials
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:80· Level 0 · nested static class
- What it is: a nested static class on E2ETestConfiguration that resolves the seeded admin login (email and password) for E2E runs, with an environment-variable override in front of a per-app default.
- Depends on:
System.Environment(BCL). It is the admin half of the credential pair; UserCredentials is its structurally identical regular-user twin. - Concept: the environment-over-default resolution taught in E2ETestConfiguration.
DefaultEmail/DefaultPasswordhave public setters so a downstream app seeds its own admin identity from a[ModuleInitializer], whileE2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORDwin when set.[Rubric §11, Security]assesses how test credentials are handled; keeping them out of committed app code and injectable per environment is the safe end of that. - Walkthrough:
DefaultEmailis"admin@localhost"andDefaultPasswordis"Admin123!"(MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:82-83);EmailandPasswordreadE2E_ADMIN_EMAIL/E2E_ADMIN_PASSWORDand fall back to those defaults (:71-75). - Where it's used: read by E2ETestBase
.LoginAsAdminAsync(MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:97-98). Both consumer suites overwrite the default from a module initializer:admin@mmca.comin Store (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:10) andadmin@adc.comin ADC (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:14).
AuthOutcome
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:4· Level 0 · internal enum
- What it is: the three verdicts a post-submit login or registration attempt can reach:
Succeeded,ErrorShown, andSilent(MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:7,:10,:13). - Depends on: nothing; a plain
internalenum, reachable from the framework's own gallery suite through theInternalsVisibleToin the package's csproj (MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12). - Concept introduced, naming the silent failure. Two of the three members are the obvious outcomes. The third,
Silent, is the one worth teaching: a submit that produced neither a navigation nor a rendered error alert (a 500 that renders nothing, a dropped request, a JS exception mid-submit) is not a success and not a reported failure, and it needs its own name precisely so the wait can refuse to treat it as either (doc,:12-13).[Rubric §14, Testability]assesses whether a harness can tell "passed" from "never actually ran"; making the third state a first-class enum member is how this package does that. - Walkthrough: each member carries a doc comment stating the signal combination behind it: the page navigated away or a signed-in state is showing (
:6-7), the auth page is still showing with an error alert on it (:9-10), or none of the three signals fired (:12-13). - Where it's used: returned by AuthOutcomeRules
.Classifyand branched on inside E2ETestBase.WaitForAuthResultAsync(MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:446,:253), and asserted directly by AuthOutcomeRulesTests.
AxeOptions
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:9· Level 0 · static class
- What it is: the shared axe-core run options that scope every accessibility scan to one documented target, WCAG 2.1 AA, so the framework gallery and all downstream apps scan against the same rule set.
- Depends on:
Deque.AxeCore.Commons(NuGet:AxeRunOptions,RunOnlyOptions,RuleOptions,MMCA.Common.Testing.E2E/Infrastructure/AxeOptions.cs:1; see primer §3). - Concept introduced, the scoped accessibility target. A raw axe run also emits "best-practice" advisories that are not conformance failures; pinning
RunOnlyto the WCAG tag set makes the gate fail only on real WCAG 2.1 AA violations (AxeOptions.cs:11-16).[Rubric §21, Accessibility]assesses whether the accessibility bar is explicit and enforced; freezing the target in one shipped object is how three repos stay honest to the same standard.[Rubric §22, Responsive/Cross-Browser]also applies through the pager exception below, which documents a specific third-party component limitation. - Walkthrough: two static presets, both read-only properties initialized once.
Wcag21Aa(AxeOptions.cs:17) setsRunOnlytoType = "tag"with the four WCAG A/AA tag valueswcag2a,wcag2aa,wcag21a,wcag21aa(:19-23). This is the target for every strict scan.Wcag21AaExceptMudPagerCombobox(:35) repeats that tag set and adds aRulesdictionary disablingaria-input-field-name(:42-45), for grid list pages whose only violation is MudBlazor's internalMudTablePager"rows per page" select. The XML doc (:26-34) records the detail: MudBlazor 9.6.0 mirrored combobox semantics onto the hidden-input presenter, the pager's own select gets no accessible name, and it is not reachable from app markup (noLabeloraria-labelparameter onMudTablePager), so this is an accepted upstream limitation. The doc warns it must be used only on a page whose sole combobox is a pager.
- Why it's built this way: shipping the options in the package rather than re-declaring them per test guarantees every consumer scans the identical rule set; the narrowly scoped pager exception keeps one known third-party gap from forcing a blanket rule-disable across all scans.
- Where it's used: passed to PageExtensions
.AssertNoAccessibilityViolationsAsyncthrough E2ETestBase.ScanAsync(strictWcag21Aa,MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:542) and.ScanGridAsync(the pager exception,:329), and directly by the*_ShouldHaveNoAccessibilityViolationsfacts on UserLoginTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:82), UserRegistrationTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:99), ProfileManagementTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:204), and PasswordResetTestsBase (MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:88,:99).
E2EPolling
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:18· Level 0 · static class
- What it is: a generic poll-until helper with two overloads, one that returns the last probed value of type
Tand one that returns a plainbool, both looping a probe until a condition holds or a deadline passes (MMCA.Common.Testing.E2E/Infrastructure/E2EPolling.cs:39,:72). - Depends on:
System.Diagnostics.Stopwatch(BCL,Stopwatch.GetTimestamp/GetElapsedTime) for the deadline arithmetic; no Playwright type appears in the signature, so it is reusable against any async probe, a page reload, a locator read, an HTTP call. - Concept introduced, asserting on the real failure, not on a bare timeout. The doc on
PollUntilAsync<T>states the design choice directly: the method always returns the last probed value, timeout or not, so a caller that times out still gets a real value to assert on and fails with that assertion's message rather than with an opaque "poll timed out" (E2EPolling.cs:26-30).[Rubric §14, Testability]assesses whether a failure explains itself; returning the last value instead of throwing on timeout is what makes that possible here. - Walkthrough:
DefaultTimeoutis 30 seconds andDefaultIntervalis 500 ms (:21,:24).PollUntilAsync<T>(probe, isSatisfied, timeout, interval, cancellationToken)(:39-44) null-checksprobeandisSatisfied(:46-47), computes the effective budget and delay from the optional parameters or the two defaults (:49-50), probes once up front, then loopsTask.Delayplus a re-probe while the condition is unmet and there is still budget left for one more full interval (:53-58), and returns the last probed value either way (:60). The bare-booloverload (:72-77) is a one-line forward to the generic overload withisSatisfiedfixed to identity (static present => present). Both overloads carry aSuppressMessageforRS0026(multiple public overloads with optional parameters), justified as grandfathered public API released after the v1.152 baseline while RS0026/RS0027 were off, so changing either signature would be a breaking change (:38,:71). - Why it's built this way: checking
Stopwatch.GetElapsedTime(started) + delay < budgetrather than just< budget(:54) avoids starting one more delay-and-probe cycle that cannot possibly land before the deadline, so the method does not overrun its stated timeout by a near-full interval. - Where it's used: by ADC's
SpeakerSelfServiceTests(MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Conference/Speaker/SpeakerSelfServiceTests.cs, 3 call sites) and 2 more sites in the ADC E2E suite.
E2ETestConfiguration
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:15· Level 0 · static class
- What it is: the single environment-variable-driven configuration surface for the whole E2E package: base URL, headless mode, timeouts, browser engine, slow-motion and trace capture, plus the nested AdminCredentials and UserCredentials.
- Depends on:
System.EnvironmentandSystem.Globalization(BCL) only. - Concept introduced, environment-driven test configuration. Every knob resolves as "read an
E2E_*environment variable, else use a default", so the same compiled suite runs against localhost on a developer box and against a CI-provisioned host with no code change. A fewDefault*properties carry public setters so a consuming app supplies app-specific defaults through a[ModuleInitializer], while environment variables always take precedence (MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:5-14).[Rubric §17, DevOps]assesses whether the suite is CI-portable and configurable outside the binary; this class is that story.[Rubric §22, Responsive/Cross-Browser]applies becauseBrowserselects the engine CI iterates over. - Walkthrough: teaching order.
DefaultBaseUrl(settable,https://localhost:7108,E2ETestConfiguration.cs:17) andBaseUrl, which prefersE2E_BASE_URL(:19-20). ADC's module initializer overrides that default tohttps://localhost:6002, the port its AppHost pins the Blazor UI to (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:13).Headlesstreats any value other than"false"as headless, compared case-insensitively (:22-23);SlowMoslows each Playwright action by an environment-set millisecond count (E2E_SLOWMO) for visual debugging, default 0 (:52-53).DefaultTimeout(30_000 ms, fromE2E_TIMEOUT,:25-26) is the general action timeout.AuthTimeout(:34-35) is a separately tunable ceiling for the slowest step, the post-auth wait, inheritingDefaultTimeoutunlessE2E_AUTH_TIMEOUTis set.AuthGraceTimeout(15_000 ms,E2E_AUTH_GRACE,:45-46) is the extra grace window that de-flakes the register/login success-detection race, the transient error-alert flash during a Server-modeforceLoad.- All four millisecond settings go through one private
ParseMilliseconds(raw, fallback, minimum)helper (:73-78). It parses withCultureInfo.InvariantCultureandNumberStyles.Float | NumberStyles.AllowThousands, so30,000reads as 30000 under every machine culture, and it returns the fallback for any value that is not finite or sits below the minimum. The minimum is 1 for the three timeouts, because Playwright reads 0 as "no timeout", and 0 forSlowMo(:9-13). Browserselectschromium(default),firefox, orwebkitfromE2E_BROWSER(:60-61);TracePathreturns a non-emptyE2E_TRACEpath or null, enabling full-speed Playwright trace capture (:70-71).
- Why it's built this way: separating
AuthTimeoutandAuthGraceTimeoutfrom the generalDefaultTimeoutis deliberate. The auth round-trip (full sign-in plusforceLoadreload plus re-render) can spike past a normal action budget on a contended CI runner, so it is tuned independently rather than by inflating every timeout in the suite. The doc ties the grace window to the TD-06/07 contention cluster and names the rejected alternative, forcing WASM, which broke login (:37-44). The parse floor exists for the same reason: a strayE2E_TIMEOUT=0or a culture-formatted number would otherwise silently disable or distort every wait in the run, so a bad value degrades to the default instead (:9-13,:73-78). - Where it's used: read throughout PlaywrightFixture (engine, headless, slow-mo) and E2ETestBase (base URL, timeouts, trace path, credentials).
ForgotPasswordPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:6· Level 0 · sealed class
- What it is: the Page Object for the shared
/forgot-passwordscreen, the entry point of the password-recovery flow. It exposes the address field, the submit button, the confirmation alert, and the return-to-login link, plus a one-callRequestResetAsyncaction. - Depends on:
Microsoft.Playwright(IPage,ILocator,AriaRole) and the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(MMCA.Common.Testing.E2E/PageObjects/ForgotPasswordPage.cs:1-2). - Concept: the Page Object Model taught in LoginPage. One locator carries a design decision rather than a selector detail:
ConfirmationAlerttargets the success alert unconditionally, and the inline comment states why, the page lands on the same success alert whether or not the address has an account (ForgotPasswordPage.cs:15-16). That is the anti-enumeration contract of ADR-091 expressed as a test affordance: there is deliberately no "unknown address" locator to assert on, because the UI must not render one.[Rubric §11, Security]assesses whether account enumeration is closed off; a Page Object that cannot express the enumerating assertion is a small structural guard on that.[Rubric §28, Front-End Testing]applies as with every Page Object here. - Walkthrough: a private
IPagefield set in the constructor (ForgotPasswordPage.cs:8-10);EmailFieldlocated by label andSubmitButtonby its accessible name "Send Reset Link" (:12-13);ConfirmationAlertas MudBlazor's.mud-alert-text-successclass (:16);BackToLoginLinklocated by link role, with the comment recording that "Back to Sign In" is a MudButton withHrefand therefore renders as an<a>(:18-19).GotoAsyncfull-loads/forgot-passwordand waits for interactivity (:21-22).RequestResetAsyncfills the address through PageExtensions.FillAndVerifyAsyncand clicks submit (:24-28). - Where it's used: driven by PasswordResetTestsBase for the unknown-address confirmation fact and the a11y fact (
MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:44-58,:79-89), and by the framework's own gallery suite, which exercises it against the backend-less gallery host including the confirmation state's own separate scan (ForgotPasswordPageE2ETests,MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:17,:28,:41,:50).
LoginPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:6· Level 0 · sealed class
- What it is: the Page Object for the shared
/loginscreen. It exposes the login form's controls as namedILocatorproperties and offersGotoAsync/LoginAsyncactions, so a test saysloginPage.LoginAsync(email, password)instead of hand-querying the DOM. - Depends on:
Microsoft.Playwright(IPage,ILocator,AriaRole) and the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(MMCA.Common.Testing.E2E/PageObjects/LoginPage.cs:1-2). - Concept introduced, the Page Object Model. A Page Object wraps one screen behind an intention-revealing API, locating controls by their accessible name (
GetByLabel("Email"),GetByRole(AriaRole.Button, Name = "Sign in to your account")) rather than by brittle CSS. That keeps tests coupled to what a user sees, not to MudBlazor's internal class names, and it centralizes each selector in one place.[Rubric §28, Front-End Testing]assesses whether E2E tests are maintainable; the Page Object is the canonical pattern for that.[Rubric §21, Accessibility]applies indirectly: locating by role and label only works if the component renders proper accessible names, so the test style pressures accessible markup. - Walkthrough: a private
IPagefield set in the constructor (LoginPage.cs:8-10); locator properties forEmailField,PasswordField,LoginButton, theErrorAlert(MudBlazor's.mud-alert-text-errorclass), and theCreateAccountLink, which the inline comment explains is a MudButton withHrefand therefore renders as an<a>located by link role (:12-18).GotoAsyncnavigates throughGotoAndWaitForBlazorAsync("/login")(:20-21);LoginAsyncfills both fields through the sharedFillFieldAsyncand then clicks (:23-28). The privateFillFieldAsyncdelegates to PageExtensions.FillAndVerifyAsync(:31-32), guarding the Blazor re-hydration race without a fixed delay. - Where it's used: instantiated by UserLoginTestsBase for the invalid-password, create-account-link, and accessibility facts, and by PasswordResetTestsBase to reach the login screen before probing the recovery entry point (
MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:28-29). - Caveats / not-in-source: the Page Object exposes no locator for the "Forgot your password?" link; the one test that asserts it locates it directly off the page (
PasswordResetTestsBase.cs:31).
ProfilePage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:6· Level 0 · sealed class
- What it is: the Page Object for the authenticated
/profilescreen, exposing the name, address, and password sections' fields and buttons as named locators. - Depends on:
Microsoft.Playwrightand PageExtensions.GotoProtectedAsync(MMCA.Common.Testing.E2E/PageObjects/ProfilePage.cs:1-2,:35). - Concept: the Page Object Model taught in LoginPage. One difference is load-bearing:
GotoAsyncusesGotoProtectedAsync("/profile")(ProfilePage.cs:34-35), client-side routing rather than a full page load, because/profileis[Authorize]and server-side rendering cannot read the JWT from browser storage, so a full load would bounce to/login.GotoProtectedAsyncfirst makes sure the Blazor runtime is loaded (loading/if it is not), re-routes through/when already on another route so the target always gets a fresh component lifecycle, then callsBlazorNavigateAsync(MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:169-200).[Rubric §28, Front-End Testing]and[Rubric §11, Security]both apply: exercising the authenticated page correctly requires respecting the client-token boundary. - Walkthrough: three commented locator groups. Name (
FirstNameField,LastNameField,SaveNameButton,:12-15); address (six fields plusSaveAddressButton,:17-24); password (CurrentPasswordField,NewPasswordFieldlocated withExact = trueso it does not also match "Confirm New Password",ConfirmNewPasswordField,ChangePasswordButton,:26-30).ErrorAlertis located by ARIA alert role rather than a MudBlazor class (:32).GotoAsyncis the client-side navigation described above (:34-35). - Where it's used: by ProfileManagementTestsBase for all six of its facts. ADC's own
ProfileManagementTestscovers the same screen without deriving from the shared base or using this Page Object (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).
PseudoLocalizedPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Globalization·MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23· Level 0 · sealed record
- What it is: the declaration of one page under the pseudo-localization gate: the
Pathto load, anEnglishProbestring owned by a resource rendered on that page, and an optionalSettleSelectorproving the page's async content arrived. - Depends on: nothing but the BCL. It is a three-parameter positional record with the third parameter defaulted to null (
MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:23). - Concept introduced, the probe string as a two-way canary. Under the
qps-Plocpseudo culture every letter gains a combining accent, so the plain en-US form ofEnglishProbeappearing on the page can only mean some render path bypassed the localizer (doc,:14-18). The same probe is then re-used under the default culture to assert it IS present, which is what keeps the probe honest: a probe that drifted from its resource value would make the leak assertion pass vacuously.[Rubric §27, i18n]assesses whether localization is machine-verified rather than eyeballed; carrying the probe next to the path is what turns a per-repo page list into a data declaration instead of test code. - Walkthrough:
Pathis loaded through the culture endpoint by the base;EnglishProbemust be a string known to come from a resource rendered on the page (doc,:14-18);SettleSelectoris a selector whose visibility proves the async content rendered (a seeded grid row, a card, a static section), and null waits only for the loading indicator to clear (doc,:19-22). - Where it's used: the element type of PseudoLocalizationTestsBase
.ScannedPages(:60). ADC declares three, home with a.location-sectionsettle selector, the speaker card grid, and the session grid (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:43-48); Store declares three with no settle selectors at all (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:41-46).
RegisterPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:6· Level 0 · sealed class
- What it is: the Page Object for the
/registerscreen, exposing the registration form (name, email, password, the Terms checkbox, plus the optional address panel) andRegisterAsyncandAcceptTermsIfShownAsyncactions. - Depends on:
Microsoft.Playwrightand PageExtensions.GotoAndWaitForBlazorAsync/FillAndVerifyAsync(MMCA.Common.Testing.E2E/PageObjects/RegisterPage.cs:1-2). - Concept: the Page Object Model taught in LoginPage, applied to a longer form.
PasswordFieldusesGetByLabel("Password", Exact = true)so it does not also match "Confirm Password" (RegisterPage.cs:15), and the optional address fields sit inside an expansion panel located by its text (:38). The Terms checkbox is the other load-bearing detail: the page renders it only when the host configuresLegal:TermsUrl, and keeps "Create Account" disabled until it is ticked, so the Page Object ticks it only when it is present, with a single count of the rendered DOM rather than a wait for absence, and withForce = truebecause MudBlazor overlays its own icon on the native input (:30-35,:65-73).[Rubric §28, Front-End Testing]applies. - Walkthrough: locator properties for the five required fields plus
RegisterButtonandErrorAlert(:12-18);EmailAlreadyRegisteredAlert, the warning alert (with the sign-in and reset-password links) the page shows when the typed address already has an account, which is deliberately NOTErrorAlert: since v1.196.0 the duplicate-address conflict renders as a warning rather than the generic red error alert (:20-25); theAlreadyHaveAccountLinksign-in link (:28);TermsCheckbox, which targets the input inside theregister-accept-termstest-id wrapper (:35); and the optional address panel and fields (:38-43).GotoAsyncfull-loads/register(:45-46);RegisterAsyncfills the five required fields through the shared helper, reusing the same password for the confirm field, callsAcceptTermsIfShownAsync, then clicks (:48-57).AcceptTermsIfShownAsyncmust run after the page is interactive: the box renders with the form, so a zero count then means the host has no Terms URL (:65-73). The privateFillFieldAsyncdelegates to PageExtensions.FillAndVerifyAsync(:79-80). - Where it's used: instantiated by UserRegistrationTestsBase for all four of its facts.
ResetPasswordPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:6· Level 0 · sealed class
- What it is: the Page Object for the shared
/reset-passwordscreen, the redemption half of the recovery flow. It exposes the address, token, and new-password fields, both outcome alerts, the return-to-login link, and two navigation entry points: the bare page and the prefilled emailed-link form. - Depends on:
Microsoft.Playwrightand the PageExtensions helpersGotoAndWaitForBlazorAsyncandFillAndVerifyAsync(MMCA.Common.Testing.E2E/PageObjects/ResetPasswordPage.cs:1-2). - Concept: the Page Object Model taught in LoginPage. What is worth teaching here is
GotoWithLinkAsync, which reproduces the way a real user arrives: the emailed link carries the address and the token as query parameters, so both fields land prefilled and the test exercises the same route the mail does (ResetPasswordPage.cs:30-36).[Rubric §24, Forms/Validation/UX Safety]assesses whether the recovery form's real arrival paths are covered; modelling both the bare and the linked entry is how this Page Object does it. - Walkthrough: a private
IPagefield set in the constructor (:8-10);EmailFieldandTokenFieldlocated by label (:12-13);NewPasswordFieldlocated withExact = true, with the comment spelling out that "New Password" is a substring of "Confirm New Password" so the default substring match would resolve to both fields (:15-17), andConfirmPasswordFieldbeside it (:18).SubmitButtonis located by the accessible name "Reset Password" (:20);ErrorAlertandSuccessAlertare the two MudBlazor alert classes (:21-22);GoToLoginLinkis again a link-role locator over a MudButton withHref(:24-25).GotoAsyncloads the bare page (:27-28);GotoWithLinkAsyncbuilds the/reset-passwordURL with the address and token query parameters, escaping both withUri.EscapeDataString(:34-36);ResetAsyncfills all four fields throughFillAndVerifyAsyncand clicks submit (:38-45). - Where it's used: by PasswordResetTestsBase for the empty-form validation fact and the a11y fact (
MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:61-76,:92-100), and by the framework's gallery suite, which additionally asserts the query-string prefill round-trip (ResetPasswordPageE2ETests,MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:17,:31,:43). - Caveats / not-in-source: no test in this package submits a genuine token. PasswordResetTestsBase's doc states why (the token only reaches the user by email, so consuming one is an app-side integration-test concern,
PasswordResetTestsBase.cs:10-16), soResetAsyncandSuccessAlertare shipped affordances that the framework's own suites do not currently drive end to end.
RoleAdminPage
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.PageObjects·MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:18· Level 0 · sealed class
- What it is: the Page Object for the role-administration screens: the
/rolesroster list and the per-role/roles/{role}permission editor, exposing the roster table, the per-role edit link, the editor's permission checkboxes, save button and saved toast, and a one-callSetPermissionAsyncaction. - Depends on:
Microsoft.Playwright(IPage,ILocator,Assertions) and PageExtensions.GotoProtectedAsync(MMCA.Common.Testing.E2E/PageObjects/RoleAdminPage.cs:39-43,:63-82). - Concept: the Page Object Model taught in LoginPage. Two comments carry the load-bearing detail.
PermissionCheckboxtargetsinput[data-permission='{permission}']because MudCheckBox splats unmatched attributes straight onto its owninput, so the permission name lands on the checkbox element itself rather than a wrapper (RoleAdminPage.cs:50-56).SetPermissionAsyncsets the checkbox withForce = truebecause MudBlazor renders the real input at zero opacity underneath its icon button, which would otherwise fail Playwright's actionability check on a control a real user can click fine (:71-74).[Rubric §28, Front-End Testing]applies as with every Page Object here;[Rubric §12, Roles/Permissions]applies because the screen under test is the role-permission editor itself. - Walkthrough: constructor takes the
IPage(:22).RoleTableis.mud-table, the signal that the roster loaded rather than failed (:25);EditLinksis everyedit-roletest-id (:28);SaveButtonis thesave-permissionstest-id (:31);SavedToastis the "Stored permissions saved." text locator, which lives in the root layout and so survives a client-side navigation (:33-37).GotoListAsync/GotoEditorAsyncnavigate to/rolesand/roles/{role}throughGotoProtectedAsync(:39-43).RowByRole(role)filters the roster rows by text (:47-48).PermissionCheckbox(permission)locates the data-attributed input (:55-56).SetPermissionAsync(permission, granted)first waits (30 second timeout) for any earlierSavedToastto clear, force-checks the box, clicks save, then waits (15 second timeout) for the toast to be visible, because the save runs on the server side of the Blazor circuit and the click alone proves nothing (:63-82). - Why it's built this way: two failure modes shape the method. The leading wait exists because a previous save's toast is still visible after
GotoEditorAsync, so it would satisfy the final wait before this save's PUT has landed, and two visible toasts make the text locator a strict-mode violation (:65-69). The final wait closes the other: navigating away before the PUT lands tears the circuit down, cancels the request, and the Identity service logs the write as "Operation cancelled by user"; a consumer deploy run once lost the write on all three tries before this wait was added (:77-82). - Where it's used: by ADC's
RoleAdministrationTests(MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/RoleAdministrationTests.cs, 4 call sites) andAccessibilityTests(MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs, 2 call sites).
UserCredentials
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:92· Level 0 · nested static class
- What it is: the regular (non-admin) counterpart to AdminCredentials, a nested static class on E2ETestConfiguration resolving the seeded customer login, environment override in front of a per-app default.
- Depends on:
System.Environment(BCL). - Concept: identical in shape to AdminCredentials; only the environment-variable names and the defaults differ.
- Walkthrough:
DefaultEmailis"user@localhost"andDefaultPasswordis"User123!"(MMCA.Common.Testing.E2E/Infrastructure/E2ETestConfiguration.cs:94-95);Email/PasswordpreferE2E_CUSTOMER_EMAIL/E2E_CUSTOMER_PASSWORD(:83-87). - Where it's used: read by E2ETestBase
.LoginAsUserAsync(MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:100-101); the default is overridden tocustomer@mmca.comin Store andcustomer@adc.comin ADC (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Infrastructure/TestSetup.cs:11,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/TestSetup.cs:15).
WebVitalsSample
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:76· Level 0 · sealed record
- What it is: an immutable record holding one page's measured Core Web Vitals:
Lcp,Cls,Fcp,Ttfb, andInp(milliseconds, except unitless CLS). - Depends on:
System.Text.Json.Serialization.JsonPropertyName(BCL) for the lowercase wire names. - Concept introduced, the vitals value object. Each property is
init-only with a short JSON name (lcp,cls,fcp,ttfb,inp,MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:78-86), so the record deserializes directly from thewindow.__vitalsJSON the browser observers accumulate.[Rubric §23, Front-End Performance]assesses whether client-side performance is measured; this record is the typed shape those measurements land in. - Walkthrough: a sealed record with five
initdoubles and no behavior (WebVitalsCollector.cs:76-87). It is the deserialization target of WebVitalsCollector.CollectAsync, which falls back to a fresh all-zero instance when the JSON deserializes to null (:56). - Where it's used: produced by WebVitalsCollector
.CollectAsync, wrapped by WebVitalsArtifact for the JSON artifact, asserted by WebVitalsBudget.AssertWithinBudget, and returned to the caller by WebVitalsPageExtensions.MeasureWebVitalsAsyncfor any further app-specific assertion.
AuthOutcomeRules
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:28· Level 1 · internal static class
- What it is: the decision behind E2ETestBase
.WaitForAuthResultAsync, extracted as a single pure function that maps three observed browser signals onto an AuthOutcome. - Depends on: AuthOutcome only. No Playwright, no xUnit: that is the point.
- Concept introduced, extracting the decision out of the wait. The surrounding wait is inherently browser-bound (three racing Playwright waits), which makes its verdict hard to test. Pulling the classification into a parameterless-of-browser function makes the interesting case, the silent failure, assertable in a unit test with no server that has to be staged into failing without rendering anything. The type doc spells out the bug this closes: a submit that failed with neither a navigation nor a rendered error alert used to leave the wait returning normally, the caller's follow-up interactivity wait was already satisfied by the still-rendered auth page, and login and registration then reported success on a sign-in that never happened (
MMCA.Common.Testing.E2E/Infrastructure/AuthOutcomeRules.cs:20-26).[Rubric §14, Testability]assesses whether logic is separable from the infrastructure it runs on;[Rubric §15, Best Practices & Code Quality]applies because the precedence rule now lives in one readable place instead of inside aTask.WhenAnycontinuation. - Walkthrough: one method.
Classify(bool navigatedAway, bool errorAlertVisible, bool logoutVisible)(:38) returnsSucceededwhen the page navigated away from the auth page OR the signed-in state's logout control is showing (:40-43), and otherwiseErrorShownif an error alert is visible,Silentif not (:45). The precedence is deliberate and documented (:30-34): a completedforceLoadis unambiguous, so an error alert flashed on the way out is not a failure, and a visible logout button is the same verdict reached through interactivity instead of through navigation. - Why it's built this way: the E2E package deliberately keeps its surface small, so this stays
internaland is opened to exactly one assembly throughInternalsVisibleToonMMCA.Common.UI.E2E.Tests(MMCA.Common.Testing.E2E/MMCA.Common.Testing.E2E.csproj:12), which is the framework's own gallery test project. That is the narrow version of "make it testable" rather than widening a shipped public API for a test. - Where it's used: called once, by E2ETestBase
.WaitForAuthResultAsync(MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:441-444), and covered directly by six browser-free facts in AuthOutcomeRulesTests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/AuthOutcomeRulesTests.cs:15,:22,:29,:34,:39,:44). - Caveats / not-in-source: both the class doc and the test class describe the rule as a "four-way classification" (
AuthOutcomeRules.cs:18,AuthOutcomeRulesTests.cs:8), while AuthOutcome declares three members; the code is the authority here.
PlaywrightFixture
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6· Level 1 · sealed class
- What it is: the xUnit collection fixture that owns the Playwright driver and one launched browser for the whole E2E collection, selecting the engine from the environment.
- Depends on:
Microsoft.Playwright(IPlaywright,IBrowser,BrowserTypeLaunchOptions) and xUnit'sIAsyncLifetime(MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:1-2). It reads E2ETestConfiguration. - Concept introduced, the shared browser fixture. Launching a browser is expensive, so one instance is created once per test collection and shared across every test rather than created per test.
[Rubric §22, Responsive/Cross-Browser]assesses cross-engine coverage, and the inline comment names that rubric category explicitly (PlaywrightFixture.cs:15-16): environment-selecting the engine here is what lets CI run the identical suite once per browser.[Rubric §14, Testability]applies too, since sharing one costly resource keeps the suite fast. - Walkthrough:
PlaywrightandBrowserare public properties with private setters (:8-9).InitializeAsynccreates the driver (:13), switches onE2ETestConfiguration.Browser.ToUpperInvariant()to pick Firefox, WebKit, or (for any unrecognized value) Chromium (:17-22), and launches it withHeadlessandSlowMofrom configuration (:24-28).DisposeAsyncsuppresses finalization, then disposes the browser and the driver defensively (:31-44): the browser is disposed only through anis { } browserpattern match and the driver through a null-conditional call, because a failedInitializeAsync(missing browser binaries, a launch timeout) leaves both null despite thenull!declarations, and the resultingNullReferenceExceptionfrom disposal used to replace the real launch error in the run output (:35-37). - Where it's used: bound to the collection by E2ETestCollection and injected into every E2ETestBase subclass, which opens a fresh browser context per test off this shared
Browser.
WebVitalsArtifact
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90· Level 1 · sealed record
- What it is: the small envelope record written to disk as
web-vitals-{label}.json: aLabel, the pagePath, and the measured WebVitalsSample. - Depends on: WebVitalsSample; serialized with
System.Text.Json. - Concept: the citable-artifact wrapper. Pairing the raw vitals with the label and path they were taken on makes the JSON file self-describing for a CI reviewer.
[Rubric §23, Front-End Performance]assesses whether performance evidence is captured and traceable; the envelope is what makes an uploaded artifact interpretable. - Walkthrough: a three-parameter positional sealed record (
MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:90), constructed inside WebVitalsCollector.WriteArtifactAsyncand serialized with the sharedWriteIndented = trueoptions (:37,:69-71). - Where it's used: only by WebVitalsCollector
.WriteArtifactAsync.
WebVitalsBudget
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:103· Level 1 · sealed record
- What it is: a per-page Core Web Vitals budget (five ceilings) plus the assertion mechanics every consumer's budget test shares: format the sample as one citable line, then assert each metric is within its ceiling.
- Depends on: WebVitalsSample,
AwesomeAssertions(theShould().BeLessThanOrEqualTo(...)calls,MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:4), andSystem.Globalizationfor culture-invariant formatting (:1). - Concept introduced, the shared budget with consumer-owned numbers. WebVitalsCollector measures; this record decides what counts as too slow, and the split is deliberate: the mechanics (which metrics, how they are formatted, how a missing INP sample is treated) belong in the framework, while the numbers stay with each consumer, whose runners and hosting differ and therefore whose calibrated maxima differ (
WebVitalsCollector.cs:92-97). The defaults are the Core Web Vitals "good" band, so a consumer that measured nothing tighter can take the record as-is.[Rubric §23, Front-End Performance]assesses whether front-end performance carries an enforced budget rather than an occasional audit;[Rubric §12, Performance & Scalability]is the tag the source itself uses for the collector pair.[Rubric §17, DevOps]applies because in both consumer repos these assertions ride the deploy-gating chromium E2E leg. - Walkthrough: a positional record with five defaulted parameters,
Lcp = 2500,Fcp = 1800,Ttfb = 800,Cls = 0.1,Inp = 500(WebVitalsCollector.cs:103-108), all milliseconds except the unitless CLS. Two members.- The static
Describe(label, path, sample)(:118) renders one invariant-culture line,[web-vitals:{label}] path=... LCP=...ms FCP=...ms CLS=... TTFB=...ms INP-sample=...ms, with CLS at three decimals and the rest at zero (:122-124), which is the record a reviewer greps for next to the uploaded JSON artifact. AssertWithinBudget(sample, label, path, writeLine = null)(:137) invokes the optional sink with that line (normallyITestOutputHelper.WriteLine,:141), then asserts LCP, FCP, TTFB, and CLS against their ceilings (:143-146). INP is asserted only whensample.Inp > 0(:148-151), because no interaction clearing the collector's 16 ms event threshold leaves the sample at 0, and 0 must read as neither a pass-by-absence nor a failure. Failure text comes from the privateMessagehelper, which names the metric, the measured value, the ceiling, and the page path (:154-157).
- The static
- Why it's built this way: keeping the numbers consumer-side while shipping the assert body is what lets ADC and Store hold different calibrated budgets without either repo re-deriving the INP-zero rule or the message format. The 0-INP carve-out is the subtle one, and it is pinned by its own unit test rather than left to a comment (WebVitalsBudgetTests,
MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:59-63). - Where it's used: ADC holds one static default instance and takes the framework numbers as-is (
MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:36, re-asserted at:93); Store constructs a default one per measurement (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42,:89). The framework's own gallery suite instead constructs one from local constants tuned for the backend-less host (WebVitalsE2ETests,MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:17-21,:29-30), and WebVitalsBudgetTests covers the record's mechanics without starting a browser (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsBudgetTests.cs:12).
PageExtensions
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:23· Level 2 · static class
- What it is: the interactivity toolbox of the E2E package: C#
extension(T)members over Playwright'sIPageandILocatorthat wait for Blazor to become interactive, navigate its InteractiveAuto pages correctly, fill and click through the re-hydration race, drive a MudDataGrid list page (search, delete-confirm, settle), and run an axe-core accessibility scan. - Depends on:
Microsoft.Playwright(IPage,ILocator,Assertions),Deque.AxeCore.Playwright/Commons(RunAxe,AxeRunOptions), andSystem.Text.RegularExpressions(MMCA.Common.Testing.E2E/Infrastructure/PageExtensions.cs:1-5). It throws AccessibilityViolationException. - Concept introduced, waiting for Blazor InteractiveAuto interactivity. The apps render with InteractiveAuto plus prerendering, so a page first appears as static HTML before the WASM runtime (or the SignalR circuit) wires event handlers; a click or fill that lands in that window is silently ignored (
PageExtensions.cs:9-14). The load-bearing detail is that "the runtime loaded" is a false green:window.Blazor._internalis set before components have attached handlers, so the wait has a second phase keyed on thedata-mmca-interactiveattribute thatMmcaThemeProvidersin MMCA.Common.UI stamps on the document element from its first interactive render (:41-48, remarks at:68-84). Signal-based waits instead of fixed sleeps are the difference between a flaky suite and a deterministic one.[Rubric §28, Front-End Testing]assesses whether the suite is reliable against real render timing;[Rubric §21, Accessibility]applies through the axe scan;[Rubric §22, Responsive/Cross-Browser]because the same waits must hold on all three engines. The type also demonstrates theextension(T)member syntax used across the framework (see primer §4). - Walkthrough: four private script constants and one public selector constant, then two extension blocks.
- The constants.
MudDataGridRowSelectoris the public default data-row selector for a MudDataGrid in table layout (:29).BlazorRuntimeLoadedPredicateprobes for any truthywindow.Blazor._internaland is shared by the wait and byGotoProtectedAsync's readiness probe so the two cannot drift (:31-39).InteractiveMarkerPredicateprobes for thedata-mmca-interactiveattribute (:47-48).TimedSettleScriptis the marker-free settle, two animation frames plus a 500 ms delay, for a click that may land on a page with no marker at all (:50-57);SingleFrameSettleScriptis the one-frame flush used once the marker has already confirmed interactivity (:60). extension(IPage page)(:62).WaitForBlazorAsyncruns the two-phase wait then flushes one frame (:85-97).GotoAndWaitForBlazorAsyncnavigates, waits forLoadState.Loadrather thanNetworkIdle(which never settles under a persistent SignalR socket), then waits for interactivity (:103-110).BlazorNavigateAsyncdrives Blazor's client-side router throughBlazor.navigateTo, tolerating the context-destroyed race aforceLoadcan cause, then pollswindow.location.pathnameinstead ofWaitForURLAsync(whose default Load wait hangs on a same-document navigation) and re-asserts interactivity, retrying once if that itself races a reload (:120-160). The path may carry a query string or fragment: it is null-checked, only its path portion (split off at?and#) is awaited becausewindow.location.pathnamenever carries them, and the value reaches the page as a script argument (p => Blazor.navigateTo(p)) rather than being interpolated into the script, so quotes in a path are safe.GotoProtectedAsyncreaches an[Authorize]page by first ensuring Blazor is up (loading a public page when it is not) and re-routing through/so the target always gets a fresh component lifecycle, then client-navigating (:169-200).WaitForPageAndBlazorAsynccovers a full-page navigation's load-plus-timed-settle (:206-215).AssertNoAccessibilityViolationsAsyncfirst waits up to 2 seconds for running CSS transitions to finish (onlyCSSTransitionanimations, because an infinite spinner animation would never end; a timeout is swallowed and the scan runs anyway), since a MudBlazor filled button fades its background over 250 ms and axe sampling mid-fade reports a contrast violation no user sees settled (:320-334). It then runsRunAxe(with optional AxeOptions), returns early on zero violations, and otherwise builds a per-node summary, each node line carrying the compacted HTML, the axe target selector and the check messages, and throws AccessibilityViolationException (:316-357).- The list-page trio, also on
IPage.SearchAndWaitForRowAsyncsettles the grid, fills the search field throughFillAndVerifyAsync, then waits for a row containing the term (:242-258); its remarks record why each of the three steps replaced a hand-rolled per-page copy, one of which slept a fixed 1.5 seconds (:217-237).ConfirmDeleteAsyncclicks a delete affordance, confirms the dialog bydata-testidrather than by a MudBlazor filled-button class (which silently changed meaning when a dialog gained a second filled action), and settles the resulting grid reload (:275-294, rationale at:260-271).WaitForGridToSettleAsyncis a plain (non-async) method that returns theToHaveCountAsyncassertion'sTaskdirectly, waiting for zero[role='progressbar']elements, and deliberately does NOT wait for a row, because an empty result set is a legitimate state (:296-308). extension(ILocator locator)(:360).FillAndVerifyAsyncfills, then auto-waitsToHaveValueAsync, and if the value was wiped by re-hydration it clears the field, re-types character by character with a 20 ms delay, and re-asserts (:372-391). This is the single shared fill helper the base and the Page Objects all call.ClickAndVerifyAsyncwaits for interactivity, then clicks and waits a third of the timeout for the expected effect, up to three clicks in total (maxAttempts = 3,:415), the last of which lets the assertion throw with full diagnostics (:406-437). An effect slower thantimeout / 3is treated as a swallowed click and re-issued, so a non-idempotent submit that can take longer than that must be called with a largertimeout(remarks at:398-404).ClickAndWaitForUrlAsyncclicks a navigating link and re-clicks until the URL matches the supplied regular expression, for grid rows whose cells wrap content inMudLinkso a row-center click lands on padding (:449-472).- The private
CompactHtmlcollapses a violating node's markup to one trimmed line, truncated at 220 characters, so the failure message points at the exact offending element (:481-490). The privateDescribeChecksappends axe's own check messages after the target (for color-contrast, the measured ratio and the foreground and background colors) joined by;, or nothing when there are none, so an intermittent violation can be diagnosed from the failure text alone (:498-507).
- The constants.
- Why it's built this way: the fill and click helpers exist because InteractiveAuto's prerender-then-hydrate model makes a bare fill or click a race on a fast host; auto-waiting assertions with a bounded re-type or re-click are strictly safer than fixed delays, since they succeed as soon as the value or effect appears. The interactivity marker is the honest gate, so a page that never stamps it fails the wait rather than proceeding to click dead controls (
:82-83). Two[SuppressMessage]attributes document analyzer false positives across theextension(T)boundary: CA1708 on the class, where the compiler-generated grouping members read as case-colliding (:15-18), and IDE0051 for the private constants and forCompactHtmlandDescribeChecks, which the SDK 10.0.201+ analyzer cannot see being referenced from inside an extension block (:19-22,:477-480,:492-497). - Where it's used: throughout the Page Objects (ForgotPasswordPage, LoginPage, ProfilePage, RegisterPage, ResetPasswordPage), inside E2ETestBase (
FillFieldAsync,ScanAsync,ScanGridAsync, the navigation helpers), by WebVitalsPageExtensions, and directly by every workflow base in this group. The list-page trio is what the consumer Page Objects are built on: Store'sProductListPageandCategoryListPageand ADC's session, speaker, room, event, question, category and user list pages all delegate their search and delete to it (for exampleMMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/PageObjects/Catalog/ProductListPage.cs:21,:42, andMMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/PageObjects/Conference/Sessions/SessionListPage.cs:39,:51).
E2ETestCollection
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:48· Level 2 · sealed class
- What it is: the xUnit
[CollectionDefinition]that binds PlaywrightFixture to the named"E2E"collection, so every E2E test class shares the one launched browser. - Depends on: xUnit's
ICollectionFixture<PlaywrightFixture>and the[CollectionDefinition]attribute (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:47-48). - Concept introduced, the xUnit collection fixture binding. A collection fixture is instantiated once and shared by every test class that opts into the collection by name. This class carries a
public const string Name = "E2E"(:50) used both in its own[CollectionDefinition(Name)]and in each test's[Collection(E2ETestCollection.Name)], so the string is declared once and cannot drift.[Rubric §14, Testability]assesses fixture design; a single named constant binding is the robust way to share a fixture. - Walkthrough: an otherwise empty class body carrying the collection definition and the
Nameconstant (:47-51). It exists purely as an xUnit marker, and it lives in the same file as the fixture it binds. - Where it's used: referenced by E2ETestBase's
[Collection(E2ETestCollection.Name)]attribute (MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:8), so every workflow base and every consumer subclass inherits collection membership. - Caveats / not-in-source: xUnit collection definitions do not cross assembly boundaries, so each consumer E2E assembly re-declares its own identically named definition over the same fixture type, and says so in its doc comment (
MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Infrastructure/E2ETestCollection.cs:3-11).
WebVitalsCollector
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20· Level 2 · static class
- What it is: the measurement infrastructure for client-side Core Web Vitals. It installs browser
PerformanceObserverscripts before first paint, reads the accumulated values back off a live page, and writes them as a citable JSON artifact. - Depends on:
Microsoft.Playwright(IPage),System.Text.Json, andSystem.IO(MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:1-5). It produces WebVitalsSample and WebVitalsArtifact, and pairs with WebVitalsBudget. - Concept introduced, in-browser performance measurement with no third-party JS. Rather than shipping an analytics SDK, it injects a small init script that installs
PerformanceObservers for LCP, CLS, FCP, and INP, each wrapped in try/catch so an engine lacking an entry type leaves that metric at 0 instead of throwing, and accumulates intowindow.__vitals(:26-35). The type doc is explicit that this is the client-side analogue of a backend load test, not a cross-engine field measurement: LCP and CLS are Chromium-only, so on Firefox and WebKit those fields stay 0 and budget assertions pass (:9-18).[Rubric §23, Front-End Performance]and[Rubric §12, Performance & Scalability]assess whether user-centric performance is measured; observing the vitals APIs directly, with no network egress, is a self-contained way to do it. The same doc states the class is only the measurement infrastructure, that WebVitalsBudget is the shared assert mechanics, and that consumers own which pages carry a budget and what the numbers are (:16-18). - Walkthrough:
InstallAsyncregisters the observers throughAddInitScriptAsyncso they are active on the next navigation (:40-44).CollectAsyncevaluates a script that stamps TTFB from Navigation Timing and returnswindow.__vitalsas JSON, deserialized into a WebVitalsSample (:47-57).WriteArtifactAsync(a non-asyncmethod that returns theFile.WriteAllTextAsynctask directly) resolves the output directory fromWEB_VITALS_OUTPUT_DIRor falls back toartifacts/under the current directory, creates it, wraps the sample in a WebVitalsArtifact, and writesweb-vitals-{label}.jsonindented (:63-72). - Why it's built this way: the observers install before the document's own scripts (through
AddInitScript) so early metrics such as FCP are not missed, and the per-observer try/catch is what makes the same code run green on all three engines despite the Chromium-only metrics. The init script is kept as one concatenated string rather than a raw literal to stay clear of the MA0136 analyzer (:22-25). - Where it's used: through WebVitalsPageExtensions
.MeasureWebVitalsAsync, which is the one call site that sequences install, navigate, collect, write and assert; the framework gallery, ADC and Store budget tests all go through it rather than calling the three methods themselves.
E2ETestBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:9· Level 3 · abstract class
- What it is: the shared base every E2E test class derives from. It gives each test a fresh isolated browser context and page off the shared PlaywrightFixture browser, plus the load-bearing auth helpers (login, register, deterministic session cleanup) and the accessibility scan helpers.
- Depends on: PlaywrightFixture, E2ETestConfiguration, AxeOptions, AuthOutcomeRules, the PageExtensions helpers, xUnit's
IAsyncLifetimeandTestContext, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Infrastructure/E2ETestBase.cs:1-4). - Concept introduced, the per-test browser context. The fixture launches one browser; this base opens a new
IBrowserContext(an isolated cookie and storage jar) per test inInitializeAsyncand disposes it inDisposeAsync, so tests cannot leak session state into each other. It is the E2E analogue of the integration-test base's per-test database reset.[Rubric §28, Front-End Testing]assesses realistic, isolated UI tests;[Rubric §21, Accessibility]applies through the scan helpers;[Rubric §14, Testability]through the shared, correctly sequenced auth helpers every workflow reuses. The auth-result handling also touches[Rubric §22, Responsive/Cross-Browser], since the same waits must survive Server-mode and WASM render timing on any engine. - Walkthrough: teaching order.
- Lifecycle. The class carries
[Collection(E2ETestCollection.Name)](:8), the fixture arrives through the constructor, and the currentPageis a protected property (:11-18).InitializeAsync(from:25) creates a context withIgnoreHTTPSErrorsand the base URL, sets the default timeout from configuration, optionally starts trace capture with screenshots, snapshots, and sources whenTracePathis set, and opens thePage.DisposeAsync(from:45) stops tracing, closes the page, and disposes the context, each step guarded by a null check because a failedInitializeAsyncleaves the fields null and the resultingNullReferenceExceptionwould otherwise mask the real setup error. The privateStopTracingAsync(from:73) carries the per-test trace policy: a plain file path keeps the single-file behavior, while a directory path writes a trace named after the current test only when that test failed (TestContext.Current.TestState?.Result == TestResult.Failed,:79-85), so a full-suite run yields just the failing traces with no overwriting. - Auth entry points.
LoginAsAdminAsyncandLoginAsUserAsync(:97-101, plainTask-returning methods that return theLoginAsynctask directly) delegate toLoginAsyncwith the AdminCredentials and UserCredentials pair.LoginAsync(from:103) first clears any existing session when a sign-out button is visible, removing theauth_access_tokenandauth_refresh_tokenlocalStorage entries and issuing aDELETE /auth/session-cookiefetch, guarded against the context-destroyed race from an in-flight logout; it then navigates to/login, fills throughFillFieldAsync, clicks, and awaitsWaitForAuthResultAsyncfollowed byWaitForInteractiveOrReloadAsync, and finishes withAcceptTermsIfPromptedAsyncwhenAcceptsTermsAfterSignInis true (:151-154).SignOutAsync(:320-331) clicks the sign-out button and then waits for the browser to actually land on/login, matching against the shared cachedLoginUrlPatternregex on a 15 second budget (:338-339); aPlaywrightExceptionthat the privateIsSupersededNavigationrecognizes, by a message carryingNS_BINDING_ABORTED,Navigation canceled by policy checkornet::ERR_ABORTED(:333-336), repeats the same wait once.RegisterNewUserAsync(from:341) generates a uniquee2e-{id}@test.comemail with the fixed passwordTestPass123!, fills the register form, ticks the Terms box through RegisterPage.AcceptTermsIfShownAsyncwhen the host shows one (:361), submits, runs the same two post-auth waits, and returns the created credentials. - Terms acceptance. A consumer whose app renders the terms acceptance gate overrides the protected virtual
AcceptsTermsAfterSignIn(defaultfalse, so a sign-in gains no step and no time,:171) in its own shared test base;LoginAsyncandRegisterNewUserAsyncthen end withAcceptTermsIfPromptedAsync(:205-234). That method first asks the privateProbeTermsPromptAsync(:240) whether the dialog will open: it GETs the signed-in user's standing fromTermsAcceptanceProbePath(virtual, default/api/Users/me/legal-acceptance,:181;nullmeans the app maps no same-origin proxy) through the page's request context, which shares the browser context's cookies, so it works in every render mode. The internal staticIsTermsPromptExpected(:272) mirrors the gate's own rule: a dialog is expected only when a current version is configured ANDisCurrentis false, and it answersnullfor an unreadable body. Afalseanswer returns at once;truewaits up to 15 seconds for the agree checkbox (TermsPromptExpectedTimeoutMs,:13), andnull(probe disabled, error status, odd body) falls back to a 10 second bounded wait (TermsPromptFallbackTimeoutMs,:14). A dialog that never appears is not a failure, because the gate renders nothing when its own read fails. Otherwise the method force-checks the box (MudBlazor overlays its own icon on the native input), clicks Accept, and waits for the dialog to hide. - Post-auth robustness.
WaitForInteractiveOrReloadAsync(from:398) waits for interactivity and, on either aPlaywrightExceptionor aTimeoutException, reloads once and re-waits rather than watching the same stuck boot; the comment records why both exception types are caught (Playwright'sTimeoutExceptionderives fromSystem.TimeoutException, notPlaywrightException, so an earlier single catch skipped the retry entirely) and why a reload beats a re-wait (the framework assets are now HTTP-cached).WaitForAuthResultAsync(from:419) starts three signals throughTask.WhenAny, leaving the auth page, the logout button appearing, or an error alert appearing, observes the two losers so their timeout faults never resurface as unobserved-task exceptions in an unrelated test, and then classifies the settled state through AuthOutcomeRules.Classify.Succeededreturns; the other two outcomes get one grace window, after whichErrorShownthrows anInvalidOperationExceptioncarrying the alert text andSilentthrows one naming the auth path, the total budget and the current URL.AuthSucceededWithinGraceAsync(from:487) implements that window, falling back to the logout-button signal when no navigation occurs. - Sign-out button name. Every "signed in" check in the base (the pre-login cleanup probe,
SignOutAsync,WaitForAuthResultAsync) locates the button by the accessible name "Logout", the name the apps' app bar now renders. Helpers.NavigateAndWaitAsync(:507), the shared staticFillFieldAsyncdelegating to PageExtensions.FillAndVerifyAsync(:515),UniqueId(:518), and the two scan helpers.ScanGridAsync(from:529) waits for a visible data row and for zero[role='progressbar']elements, then scans with AxeOptions.Wcag21AaExceptMudPagerCombobox;ScanAsync(from:539) applies the progressbar guard only and scans strictly withWcag21Aa. - Dark-theme helpers.
SeedDarkThemeAsyncadds anmmca_theme=darkcookie to the browser context, read by the Common UI'stheme.json the next page load.WaitForDarkModeAsyncthen waits for the theme toggle's accessible name to flip to "Switch to light mode", the signal thatThemeServicehas initialized from the cookie andMudThemeProviderhas re-rendered with the dark palette; because the layout renders the toggle twice (the app bar and the mobile nav top row), the wait targets the desktop cluster's copy (.appbar-icon-actions.First), and the expected name is the English resource, matching the default E2E culture.
- Lifecycle. The class carries
- Why it's built this way: the auth helpers encode hard-won timing knowledge once (the
forceLoadreload, the Server-versus-WASM hydration lag, the cookie-and-localStorage dual session store), so every consumer workflow inherits a deterministic sign-in instead of re-deriving the races. Clearing both token stores is essential: the Blazor Server host is cookie-only, so a localStorage clear alone would leave the next login authenticated as the wrong user (:100-105). Sign-out is a base helper for the same reason: a workflow that clicked the button and then waited forLoadState.Loadwas waiting on the CURRENT document, whose load event had already fired, so the nextgotodied with "execution context was destroyed" or "interrupted by another navigation"; the URL wait fixes that once for every caller, and the one-shot retry covers the engines that report the superseded navigation as an error: Firefox surfaces the abandoned first request asNS_BINDING_ABORTEDwhen the logoutforceLoadis superseded mid-flight by the app's own redirect onto/login, and WebKit raises the same supersession as "Navigation canceled by policy check" now that sign-out runs through the same-origin API proxy, while chromium raisesnet::ERR_ABORTED("maybe frame was detached?") when a password change has just revoked the session and the app's own redirect onto/loginovertakes the sign-out (remarks ending at:318). A sign-out that never reaches/loginstill fails on the second wait. The scan split lets grid pages accept the documented pager-combobox exception while every other page stays strict, and the grid wait keys off a data row rather than the loading bar hiding, which would resolve instantly before the transient unnamed progressbar even appears (:353-361).SeedDarkThemeAsyncsets the cookie directly instead of driving the UI toggle so a workflow test can start already in dark mode without spending a page interaction on it;WaitForDarkModeAsyncis a separate call, not folded into the seed helper, because the caller must navigate between seeding the cookie and checking the applied theme. - Where it's used: the base class of all eight workflow bases in this unit (AuthorizationTestsBase, LogoutTestsBase, PasswordResetTestsBase, ProfileManagementTestsBase, PseudoLocalizationTestsBase, UserLoginTestsBase, UserPreferencesTestsBase, UserRegistrationTestsBase) and, through them and directly, every E2E test class in the ADC and Store suites (for example ADC's own
ProfileManagementTests, which derives from this base rather than the shared profile workflow,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8).
WebVitalsPageExtensions
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Infrastructure·MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:13· Level 3 · static class
- What it is: the one-call Core Web Vitals measurement, an
extension(IPage)block with two members that install the collector, load the page, drive an interaction (inline-optional in one, scripted in the other), collect, write the artifact, and assert the sample against a budget. - Depends on: WebVitalsCollector, WebVitalsBudget, WebVitalsSample, PageExtensions
.GotoAndWaitForBlazorAsync, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Infrastructure/WebVitalsPageExtensions.cs:1). - Concept introduced, shipping the ORDER, not just the parts. The collector, the sample and the budget are each individually usable, and every consumer used to wire them together in a private
MeasureAsync. The step order is the load-bearing part: the observers must be installed BEFORE the navigation or LCP, FCP and TTFB are never recorded for that load, which is exactly the kind of detail a per-repo copy gets wrong once and then carries (doc,:5-12). Lifting the sequence into the package turns an ordering convention into a compiler-checked call.[Rubric §23, Front-End Performance]assesses whether the performance gate is trustworthy;[Rubric §15, Best Practices and Code Quality]applies because three copies collapsed into one;[Rubric §33, Developer Experience]applies because a consumer now writes one line per measured page. - Walkthrough:
MeasureWebVitalsAsync(label, path, budget, writeLine = null, interactionPlaceholder = null, interactionText = "test")(:32-38) null-checks the page and the budget (:40-41), installs the observers (:43), navigates throughGotoAndWaitForBlazorAsync(:44), and, when an interaction placeholder is supplied, clicks and fills that input if it is visible and then waits 300 ms so the event-timing observer records what it just saw (:46-57). It then collects the sample (:59), writes the artifact (:60), callsbudget.AssertWithinBudget(...)with the optional output sink (:62), and returns the sample for any further app-specific assertion (:63).MeasureWebVitalsWithInteractionAsync(label, path, budget, interaction, writeLine = null)(:84-89) is the second member, added alongside the first: it null-checks the page, the budget and the interaction delegate (:91-93), installs the observers and navigates in the same order (:95-96), then runs the caller-suppliedFunc<IPage, Task>unconditionally (:98, no placeholder-visibility check) before the same 300 ms wait, collect, write and budget assertion (:100-107). It carries its own name rather than overloadingMeasureWebVitalsAsync, whose optional parameters would make the two ambiguous at the call site (:66-73). - Why it's built this way: the interaction is best-effort by contract in
MeasureWebVitalsAsync; an absent or invisible field is skipped, and if no event clears the collector's 16 ms threshold INP stays 0 and its assertion is skipped by the budget (doc,:24-29).MeasureWebVitalsWithInteractionAsynctrades that best-effort placeholder lookup for an arbitrary scripted interaction (a click, a keystroke, anything the event-timing observer can time), still leaving INP at 0 rather than a failure when the interaction clears nothing over the threshold (:77-81). Together the two members cover load-only pages, pages with a single fillable field, and pages whose interaction is not a simple placeholder fill. - Where it's used:
MeasureWebVitalsAsyncis called by every budget test in the workspace. The framework gallery measures three gallery pages against a locally tuned budget (WebVitalsE2ETests,MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitals/WebVitalsE2ETests.cs:47,:48,:56); ADC funnels four pages through one private wrapper that re-asserts the returned sample at the call site (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:90-93); Store calls it directly for home, catalog (with a search-box interaction) and login (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:42,:52,:62).MeasureWebVitalsWithInteractionAsynchas no caller outside its own defining file yet. - Caveats / not-in-source: the type doc calls the output a "dated" artifact (
:8); the file name WebVitalsCollector actually writes isweb-vitals-{label}.json, with no date component (MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:70).
KeyboardOperabilityTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Accessibility·MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:41· Level 4 · abstract class
- What it is: the reusable keyboard-operability workflow base. Authored once, it asserts four WCAG-style keyboard behaviors that a consumer app opts into by declaring its pages and one dialog: the skip link is the first Tab stop and moves focus into the main landmark, an open dialog traps Tab and Shift+Tab, Escape closes the dialog and returns focus to the opener, and the first few Tab stops on each page show a visible focus indicator.
- Depends on: E2ETestBase, PlaywrightFixture, PageExtensions (
GotoAndWaitForBlazorAsync), E2EPolling (PollUntilAsync), AwesomeAssertions, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Accessibility/KeyboardOperabilityTestsBase.cs:1-7). - Concept: the authored-once workflow base taught in AuthorizationTestsBase, here for keyboard accessibility instead of route security.
[Rubric §21, Accessibility]assesses whether the UI is operable without a pointer; axe scans cannot see a focus trap or a skip link's destination, so this base drives real key presses. - Walkthrough: the subclass supplies three abstract members,
PagePaths(the pages whose skip link and focus indicators are checked,:82),DialogPagePath(:85) andDialogOpener(the control that opens arole="dialog"on Enter,:91). Five virtual hooks have defaults:Dialog(the firstrole="dialog"element,:97),FocusReturnTarget(defaults to the opener,:103),ArrangeAsync(a no-op run before each fact's first navigation, for sign-in or data setup,:110),NavigateAsync(a full load that waits for Blazor, overridable to route through a client-side navigation that keeps an authorized session,:119) andCleanupAsync(a no-op,:128). Four[Fact]s (:130-139) each run through the privateRunArrangedAsync(:144), which callsArrangeAsyncinside atryandCleanupAsyncinfinally, so an arrangement that throws part-way is still undone.SkipLink_IsFirstTabStop_AndMovesFocusIntoMainLandmarkfocuses a script-only probe at the top of the document, presses Tab, requires the focused element to be an anchor whosehrefcontains#, presses Enter, and requires focus to land insidemainor[role="main"].OpenDialog_TrapsTabFocusopens the dialog by keyboard and presses Tab then Shift+TabTrapTabPresses(12,:44) times each, waiting for focus to stop moving between presses (FocusSettledScript,:56) and requiringdocument.activeElementto stay inside the dialog.Escape_ClosesDialog_AndReturnsFocusToOpenerpresses Escape and expects the dialog count to reach 0 andFocusReturnTargetto be focused, withinFocusTimeoutMs(10 seconds,:50).FocusedElement_ShowsVisibleFocusIndicatortabsIndicatorTabStops(3,:47) stops on each page and reads the focused element's computed outline and box-shadow throughFocusStyleScript(:68). - Why it's built this way: both focus polls use
E2EPolling.PollUntilAsyncat a 100 ms interval (FocusSettlesAsync,:279) because focus moves asynchronously after a keypress in an interactive Blazor page, so a single read races it. A box-shadow counts as a focus indicator only when it differs from the same element's blurred shadow, since an elevated button carries a resting shadow whether focused or not;FocusStyleScriptfinishes running CSS transitions before each read so a blur does not report the focused value mid-transition. The skip-link and focus-indicator facts assertPagePathsis non-empty, the same non-vacuity guard AuthorizationTestsBase uses, so a subclass that declares no pages fails instead of passing silently.ArrangeAsyncruns inside thetryfor the same reasonCleanupAsyncmust tolerate a partial arrangement. - Where it's used: subclassed by ADC's
MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Engagement/LiveModerationKeyboardTests.csand by the Common UI E2E suite (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/KeyboardOperabilityE2ETests.cs,.../Accessibility/FocusIndicatorRuleE2ETests.cs).
AuthorizationTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:18· Level 4 · abstract class
- What it is: the reusable authorization workflow fitness base, authored once and re-run as a thin subclass per repo. It asserts that anonymous users are redirected off protected paths, that public paths stay reachable, that a registered non-admin can reach an authenticated page, and that a non-admin probing admin routes gets the Forbidden page.
- Depends on: E2ETestBase, PageExtensions (
GotoAndWaitForBlazorAsync,GotoProtectedAsync), AwesomeAssertions, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/AuthorizationTestsBase.cs:1-6). - Concept introduced, the authored-once workflow fitness base. This is the pattern shared by all eight bases in this unit: the framework owns the assertions and the SSR-versus-client-navigation mechanics, and each consumer supplies only its own route lists through abstract or virtual members, so identical security behavior is verified across repos without copying test bodies (
:10-17).[Rubric §11, Security]assesses whether authorization is actually exercised; this base machine-checks both the anonymous-redirect and the authenticated-non-admin-escalation directions.[Rubric §25, Navigation & IA]applies because it pins which routes are public and which are gated. - Walkthrough: the subclass supplies
ProtectedPathsandPublicPaths(abstract,:26,:29) and optionallyAuthenticatedUserPathandAdminPaths(virtual, defaulting to null and an empty list,:35,:44). Four facts follow.AnonymousUser_ProtectedPages_ShouldRedirectToLoginasserts each protected path bounces to/login(:46-58).AnonymousUser_PublicPages_ShouldBeAccessibleasserts each public path stays put (:60-72).RegisteredUser_AuthenticatedPage_ShouldBeAccessibleregisters a non-admin, then client-navigates throughGotoProtectedAsyncbecause SSR cannot read the JWT, passing vacuously when no path is declared (:74-93).RegisteredUser_AdminPages_ShouldBeForbiddenregisters a non-admin, then asserts each admin path renders the shared Forbidden page, matching the level-1 heading (viaGetByRole(AriaRole.Heading, new() { Level = 1 })) containing "Access Denied", with the comment noting that role denial is not a redirect so the page content is the only reliable signal (:95-120). - Why it's built this way: the two optional members use a no-dynamic-skip convention (an app with no such page simply passes) because the shipped library deliberately does not reference
xunit.v3.assertfor a declared skip (:77-78,:98-99). The non-empty assertions onProtectedPathsandPublicPaths(:49-50,:63-64) are non-vacuity guards: a repo that declares no paths fails rather than passing silently. - Where it's used: subclassed in both consumer E2E suites with that app's route lists (
MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-21,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/AuthorizationTests.cs:11-44, whose twelve-entryAdminPathscarries a comment explaining which authenticated-but-not-Organizer routes are deliberately excluded,:21-24); Store's subclass also adds one app-specific fact of its own, an anonymous order-detail deep link that must leak no order content (AuthorizationTests.cs:27-42).
LogoutTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:9· Level 4 · abstract class
- What it is: the reusable logout workflow base. It verifies that sign-out returns the user to the login screen and that a logged-out user can no longer reach a protected page.
- Depends on: E2ETestBase, PageExtensions (
WaitForBlazorAsync), andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/LogoutTestsBase.cs:1-5). - Concept: the authored-once workflow base taught in AuthorizationTestsBase.
[Rubric §11, Security]assesses session teardown; this base guards that logout genuinely revokes access rather than merely returning to a login screen visually. - Walkthrough: two facts.
Logout_ShouldRedirectToLoginPageregisters, confirms the sign-out button (accessible name "Logout") is visible, clicks it, waits for the load state, and asserts the sign-in button appears (:16-29).Logout_ShouldPreventAccessToProtectedPagesregisters, waits for interactivity (becauseRegisterNewUserAsynccan return with the button visible before JS interop is ready,:37-40), then clicks sign-out insideRunAndWaitForResponseAsyncso it blocks until the best-effortDELETE /auth/session-cookieresponse arrives (:47-51), confirms the sign-in button is visible (:54-55), and then re-requests/profileup to six times until the server redirects to/login(:64-72), falling back to a clear URL assertion if it never does (:75). - Why it's built this way: waiting for the cookie-clear response is the fix for a real full-speed race. At speed the test otherwise reaches
/profilebefore the DELETE finishes, so the HttpOnly cookie is still present and SSR re-authenticates. The bounded re-request loop converges deterministically where any slowdown (slow-mo, or even trace capture) would have hidden the race entirely (:42-46,:57-63). - Where it's used: subclassed in both consumer E2E suites (
MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/LogoutTests.cs:5,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/LogoutTests.cs:5).
PasswordResetTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:17· Level 4 · abstract class
- What it is: the reusable password-recovery workflow base covering the shared
/forgot-passwordand/reset-passwordpages: the entry point is reachable from the login screen, an unknown address gets the same confirmation as a known one, the reset form's client-side validation blocks an empty submit, and both pages are accessibility-clean. - Depends on: E2ETestBase, LoginPage, ForgotPasswordPage, ResetPasswordPage, PageExtensions, AxeOptions, and
Microsoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/PasswordResetTestsBase.cs:1-6). - Concept introduced, drawing the E2E boundary around what a browser can honestly observe. The type doc states outright that the real token round-trip is deliberately not exercised here: the token only reaches the user by email, so redeeming one is an app-side integration-test concern, and what E2E owns is the reachability of the flow, the anti-enumeration confirmation, client-side validation, and WCAG 2.1 AA conformance of both pages (
:10-16). That is a scope decision worth internalizing: a browser test that cannot reach the mailbox should assert the contract it can see rather than fake the one it cannot.[Rubric §11, Security]assesses whether the recovery flow leaks account existence; the unknown-address fact is the machine check on ADR-091's anti-enumeration rule.[Rubric §24, Forms/Validation/UX Safety]covers the empty-submit validation,[Rubric §21, Accessibility]the two scans, and[Rubric §25, Navigation & IA]the entry-point fact, since a locked-out user has no other way in. - Walkthrough: five facts, no abstract members, so a consumer subclass is a single line.
LoginPage_ForgotPasswordLink_NavigatesToForgotPasswordPage(:24-41) opens the LoginPage, asserts the "Forgot your password?" link is visible at all (the comment notes a user locked out of their account has no other entry point,:33), clicks it, and asserts the URL ends at/forgot-password(:40).ForgotPassword_WithUnknownEmail_ShowsTheSameConfirmation(:43-58) submits anunknown-{UniqueId()}@test.comaddress that certainly has no account, then asserts the positive path exactly: the success confirmation appears (:55), the URL stays on/forgot-password(:56), and the back-to-login link is visible (:57). There is no error alert and no navigation to distinguish it from a real address, which is the whole point.ResetPassword_WithEmptyForm_ShowsClientValidationErrors(:60-76) clicks submit on an untouched form; DataAnnotations blockOnValidSubmitso nothing is sent, and the fact asserts the field-level texts "Email is required" and "Reset token is required" plus staying on/reset-password(:73-75).ForgotPasswordPage_ShouldHaveNoAccessibilityViolations(:78-89) andResetPasswordPage_ShouldHaveNoAccessibilityViolations(:91-100) each load their page and scan with AxeOptions.Wcag21Aa.
- Why it's built this way: the validation fact asserts the field-level text rather than a page-level alert, and the source gives the reason: those messages are present in both render modes (Server prerender and WebAssembly) while a page-level alert is not, the same reasoning as the mismatched-password registration test (
:70-72, and UserRegistrationTestsBase.Register_WithMismatchedPasswords_ShouldShowError). The a11y scans pass the strictWcag21Aapreset explicitly rather than running an unscopedRunAxe, keeping this workflow on the same documented target as the rest of Identity (:85-88). - Where it's used: subclassed with no additions in both consumer E2E suites (
MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5,MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/PasswordResetTests.cs:5). The two Page Objects it drives are additionally exercised against the framework's own backend-less gallery host by ForgotPasswordPageE2ETests and ResetPasswordPageE2ETests (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:9,MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Auth/ResetPasswordPageE2ETests.cs:9).
ProfileManagementTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:10· Level 4 · abstract class
- What it is: the reusable profile workflow base. It verifies that name, address, and password changes persist, that the profile page loads pre-filled with the registered data, an opt-in email-change journey, and that the profile page is accessibility-clean.
- Depends on: E2ETestBase, ProfilePage, PageExtensions, AxeOptions, AwesomeAssertions, and
Microsoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/ProfileManagementTestsBase.cs:1-6). - Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a ProfilePage.
[Rubric §24, Forms/Validation/UX Safety]assesses whether edit-and-persist journeys work end to end;[Rubric §21, Accessibility]applies through the a11y fact. - Walkthrough: one virtual switch,
ProfileSupportsEmailChange, off by default (:23), and three virtual snackbar texts,NameSavedMessage("Name updated successfully."),AddressSavedMessageandEmailSavedMessage(:30,:33,:36), which a consumer whose profile page words its confirmations differently overrides. One virtual hook,FillStateAsync(ProfilePage), sets the State field during the address test: the default null-guards the page object and types "TS" into a text field, and an app whose State field is a picker overrides it to choose an option, since a read-only combobox cannot be filled (:38-49). Six facts follow.ChangeName_ShouldUpdateProfileNameclears and fills both name fields, saves, waits up to 30 seconds for theNameSavedMessagesnackbar (:69), re-navigates, and asserts the values persisted (:51-78);ChangeAddress_ShouldUpdateProfileAddressdoes the same for the five address fields, setting State throughFillStateAsync(:92), waiting onAddressSavedMessage(:96), and asserting on line 1 (:80-103). Both use Playwright's plainFillAsyncfor their text fields rather than the re-hydration-safe helper, since the profile page is reached by client-side navigation on an already interactive runtime.ChangePassword_WithValidCurrentPassword_ShouldSucceedfills the three password fields through the sharedFillFieldAsync, waits for the "Password changed successfully." snackbar, then signs out through the base'sSignOutAsyncand logs back in with the new password (:105-134, sign-out at:131).ChangeEmail_ShouldUpdateEmailis opt-in and returns immediately unlessProfileSupportsEmailChangeis overridden true; when it runs, it waits onEmailSavedMessagebefore reloading (:136-170, wait at:163).ProfilePage_ShouldLoadWithUserDataasserts the form is pre-filled from registration (:172-190).ProfilePage_ShouldHaveNoAccessibilityViolationsscans with AxeOptions.Wcag21Aa(:192-205). - Why it's built this way: each save waits for its confirmation snackbar rather than
LoadState.Loadbecause navigating away while the save is still in flight cancels the write, so without the wait the read-back could run before the save persisted (:25-29). The email-change fact is a declared opt-in rather than a DOM probe because the previous probing version passed vacuously when the field was absent, reporting coverage for a journey the app does not offer; overriding the flag makes a missing field fail loud (:17-22,:139-142). State setting is a virtual hook rather than a fixedFillAsyncbecause the field's shape is the app's choice: a text field in the default, a picker in Store, and the address journey stays shared across both (:38-44). The sign-out-then-login step delegates to E2ETestBase.SignOutAsyncinstead of clicking and waiting inline, so this workflow inherits both halves of the fix (the/loginURL wait rather thanLoadState.Load, and the one-shot retry on a superseded navigation that Firefox and WebKit report as an error) from one place; the retained comment records why the load-state wait raced the in-flight logout navigation (:125-130). - Where it's used: subclassed only by Store (
MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:5), whose one addition overridesFillStateAsyncto open its State picker over the US postal regions and click the "Texas" item in the open popover, waiting up to 5 seconds for it to show (:11-19). Store does not overrideProfileSupportsEmailChange, so the email-change fact passes without exercising a journey Store offers. ADC does not subclass this base: its profile page supports the avatar photo, password change, and account deletion but no name or address editing, soMMCA.ADC.E2E.Testswrites its ownProfileManagementTestsdirectly on E2ETestBase with a password-change fact mirroring this one, a claims-page fact, and an avatar upload-replace-remove round trip that builds its two PNGs from base64 constants so the test needs no fixture file on disk (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/ProfileManagementTests.cs:8,:26,:56,:70, constants at:13-16).
PseudoLocalizationTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Globalization·MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:52· Level 4 · abstract class
- What it is: the reusable pseudo-localization fitness gate. Each page a consumer declares is loaded under the
qps-Plocpseudo culture and asserted three ways (a visible bracket sentinel renders, the page's en-US probe does NOT render in plain English, and the page does not overflow horizontally), then re-loaded under the default culture to prove the sentinel never ships to a real locale. - Depends on: E2ETestBase, PseudoLocalizedPage, PageExtensions
.GotoAndWaitForBlazorAsync, AwesomeAssertions, andSystem.Globalization(MMCA.Common.Testing.E2E/Workflows/Globalization/PseudoLocalizationTestsBase.cs:1-5). - Concept introduced, pseudo-localization as an executable gate. A pseudo locale transforms every localized string (here by bracketing it and adding a combining accent to each letter) without needing a translator. Three properties fall out of that, and this base asserts all three: the round trip from resource file through
IStringLocalizerand the framework'sPseudoStringLocalizerto markup is genuinely happening (the sentinel is visible), no string bypassed the pipeline (the plain en-US probe is absent), and the layout tolerates the roughly 40% text expansion a real translation brings (no horizontal document overflow), which is the rubric's layout-tolerance criterion (doc,:26-41).[Rubric §27, i18n]assesses whether localization is verified end to end rather than assumed;[Rubric §22, Responsive/Cross-Browser]covers the overflow half. The transform itself is taught with PseudoStringLocalizer; the supported-culture list including the pseudo locale is SupportedCultures. - Walkthrough: six virtual knobs and two facts.
- The knobs.
ScannedPagesis the one abstract member (:60).PseudoLocaledefaults to"qps-Ploc", restated here rather than referenced because the shipped fixture library deliberately does not reference MMCA.Common.Shared (:62-66).Sentineldefaults to"[!!"(:69),OverflowTolerancePxto 1 CSS pixel so sub-pixel rounding is absorbed without admitting a genuine sideways scrollbar (:71-75),ProbeVisibleTextOnlyto false so the probe is searched in the whole document including attributes such as an input placeholder (:77-83),AssertProbePresentUnderDefaultCultureto true (:85-90), andTimeoutto 15_000 ms (:93). PseudoLocale_RendersSentinel_WithoutEnUsLeak_AndDoesNotOverflowHorizontally(:95-132) first guards non-vacuity onScannedPages(:98-99), then per page activates the culture by loading/culture/set?culture=...&redirectUri=...(:106-107), settles (:109), asserts at least one VISIBLE sentinel (:112-113), asserts the probed text does not contain the en-US probe (:117-120), and evaluatesscrollWidth - clientWidthon the scrolling element against the tolerance (:124-130).DefaultCulture_DoesNotLeakPseudoSentinel(:134-157) re-loads each page with no culture cookie (each test gets a fresh browser context from E2ETestBase), asserts the page content contains no sentinel (:144-147), and, whenAssertProbePresentUnderDefaultCultureholds, asserts the probe IS present so the leak check cannot pass vacuously (:149-155).- Two private helpers:
ProbedTextAsyncselects document content or body innerText per the flag (:164-167), andSettleAsyncwaits for the optional settle selector and then for zero[role='progressbar']elements (:174-184).
- The knobs.
- Why it's built this way: activation goes through the culture COOKIE (
GET /culture/set, the same endpoint the app's culture switcher uses) rather than a query-string culture provider, and the remarks explain why: a Blazor Server circuit takes its culture from the request that starts the circuit, which carries cookies but not the original page's query string, so a query-string-only activation would pseudo-localize the prerender and then revert on hydration (:42-51).qps-Plocis allowlisted in Development only, which is what an Aspire-launched E2E stack runs, so the gate needs no host change; public pages are the deliberate subjects because skipping login keeps the gate robust. The mechanics reference ADR-027. - Where it's used: subclassed once per consumer, each contributing only its page list. ADC takes every default unchanged and declares three public pages (
MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/PseudoLocalizationTests.cs:29, list at:39-44); Store declares three and overridesProbeVisibleTextOnlyto true so an aria-label or serialized prerender payload carrying the plain form cannot fail the gate (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/PseudoLocalizationTests.cs:25, list at:41-46, override at:53). The framework's own gallery has a separate prerender-level equivalent in PseudoLocalizationE2ETests.
UserLoginTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:10· Level 4 · abstract class
- What it is: the reusable login workflow base. Valid credentials reach the home page and show the authenticated app bar, invalid credentials show an error and stay on
/login, the create-account link navigates to/register, and the login page is accessibility-clean. - Depends on: E2ETestBase, LoginPage, PageExtensions, AxeOptions, and
Microsoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:1-6). - Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a LoginPage.
[Rubric §28, Front-End Testing]and[Rubric §11, Security]apply. - Walkthrough: four facts.
Login_WithValidCredentials_ShouldNavigateToHomePageregisters (which auto-logs in), signs out through the base'sSignOutAsync(:27), logs back in, and asserts the URL left/login, the sign-out button (accessible name "Logout") is visible, and the "Sign In" link is not (:18-41).Login_WithInvalidPassword_ShouldShowErrordrivesLoginPage.LoginAsyncwith a nonexistent account and asserts the error alert appears and the URL stays on/login(:43-57).Login_NavigateToCreateAccount_ShouldGoToRegisterPageclicks the create-account link and asserts/register(:59-71).LoginPage_ShouldHaveNoAccessibilityViolationsscans with AxeOptions.Wcag21Aa(:73-84). - Why it's built this way: the sign-out step is a single call into E2ETestBase
.SignOutAsyncrather than an inline click plus wait, and the comment above it (:23-26) keeps the reason on the call site: waiting for the logoutforceLoad's/loginURL rather thanLoadState.Loadis the fix for the sign-out-then-login race, where the current document's load event had already fired so the wait returned immediately and the pre-login cleanup evaluate died with "execution context was destroyed". - Where it's used: subclassed in both consumer E2E suites (
MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5).
UserPreferencesTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Preferences·MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:21· Level 4 · abstract class
- What it is: the reusable culture-switch and theme-toggle workflow base. It verifies that switching to Spanish localizes and persists, that toggling dark mode applies and persists, and that both controls are reachable on a mobile viewport.
- Depends on: E2ETestBase, PageExtensions (
GotoAndWaitForBlazorAsync), AwesomeAssertions, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Preferences/UserPreferencesTestsBase.cs:1-5). - Concept introduced, the self-contained preferences fitness base. Unlike the identity bases, this one needs no app-specific overrides beyond the dark-palette value of an app that re-points the palette variable: the probe page is the shared
/login, the probe string is the localized "Welcome Back" / "Bienvenido de nuevo", and persistence is the anonymous cookie pair (.AspNetCore.Cultureplusmmca_theme), all owned by Common UI in every app (:9-20).[Rubric §27, i18n]assesses whether localization actually switches and persists;[Rubric §20, Design System & Theming]covers the theme toggle;[Rubric §22, Responsive/Cross-Browser]covers the mobile-parity fact. The source doc cites ADR-027 and ADR-028 for the localization and theming mechanics. - Walkthrough: a dark-background probe script that reads
--mud-palette-background, strips whitespace, lower-cases it, and returns whether it is in theexpectedarray passed in (:25-28); a virtualDarkPaletteBackgroundslist supplies that array, defaulting to MudBlazor's own PaletteDark background as either the raw hex#1a2027or itsrgba(26,32,39,1)form (:35-42); plus desktop and mobile action-cluster locators scoped by container (.appbar-icon-actionsand.toprow-actions,:44-48) to disambiguate the duplicated NavMenu controls. Three facts follow.CultureSwitch_ToSpanish_ShouldLocalizeAndPersistopens the Language menu and clicks the Spanish item (its label carries the accented spelling in source) located as.mud-popover-open .mud-menu-item(the popover carries that class only once Blazor interactivity attached, and the items are not.mud-list-itemand carry no menuitem role), then asserts the Spanish probe survives a fresh full page load (:50-73).ThemeToggle_ToDark_ShouldApplyAndPersistclicks the title-stable toggle (its aria-label flips with state, its title does not), asserts the palette variable flipped throughAssertDarkPaletteAsync, assertslocalStorageholdsmmca_themeset todark, then reloads and re-asserts (:75-94).MobileViewport_CultureAndTheme_ShouldBeReachablesets a 390x844 viewport and asserts the controls come from NavMenu's top row, then actually toggles the theme there rather than only checking that it rendered (:96-110). The privateAssertDarkPaletteAsyncpolls the probe script withDarkPaletteBackgrounds.ToArray()as its argument and a 15 s ceiling (:112-116). - Why it's built this way: the mobile fact pins the v1.103.0 regression where the controls lived only in the app bar, hidden below 1024px; the selectors mirror the gallery's own MobileTopRowE2ETests exactly because the MudMenu activator exposes no literal
aria-labelattribute, so raw CSS attribute selectors do not match it (:14-19). The dark value is a virtual list rather than a hard-coded pair because an app's stylesheet may re-point the variable at its own dark surface; without the override the theme facts time out while dark mode works (:35-41). - Where it's used: subclassed in both consumer E2E suites. Store overrides
DarkPaletteBackgroundswith#1a1416, because its stylesheet re-points--mud-palette-backgroundat the brand's dark page (MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:15, override at:17, rationale at:10-14). ADC keeps the default palette and only opts into its terms acceptance gate throughAcceptsTermsAfterSignIn(MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Preferences/UserPreferencesTests.cs:10,:14).
UserRegistrationTestsBase
MMCA.Common.Testing.E2E ·
MMCA.Common.Testing.E2E.Workflows.Identity·MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:10· Level 4 · abstract class
- What it is: the reusable registration workflow base. Valid data navigates to the home page and logs the user in, mismatched passwords show the inline validation message and stay on
/register, a duplicate email shows a warning naming the way out (sign in or reset the password), and the register page is accessibility-clean. - Depends on: E2ETestBase, RegisterPage, PageExtensions (
FillAndVerifyAsync), AxeOptions, andMicrosoft.Playwright(MMCA.Common.Testing.E2E/Workflows/Identity/UserRegistrationTestsBase.cs:1-6). - Concept: the authored-once workflow base taught in AuthorizationTestsBase, here driving a RegisterPage.
[Rubric §24, Forms/Validation/UX Safety]assesses client-side validation and duplicate handling;[Rubric §21, Accessibility]applies through the a11y fact. - Walkthrough: four facts.
Register_WithValidData_ShouldNavigateToHomePageregisters a unique user through the Page Object and asserts the URL left/registerand the app-bar button named "Logout" is visible (:17-34).Register_WithMismatchedPasswords_ShouldShowErrorfills every field throughFillAndVerifyAsync, ticks the Terms box throughRegisterPage.AcceptTermsIfShownAsync(on a host with a Terms URL the submit stays disabled until it is ticked, so the password mismatch is then the only reason the submit is refused), submits exactly once, and asserts the inline "Passwords do not match" validation text plus staying on/register(:36-67).Register_WithDuplicateEmail_ShouldShowErrorregisters, then re-registers the same email and asserts theEmailAlreadyRegisteredAlert(a warning naming the way out, sign in or reset the password, rather than the generic red error alert the other server-side failures use) is visible, contains the email, and that the page stays on/register(:69-87).RegisterPage_ShouldHaveNoAccessibilityViolationsscans with AxeOptions.Wcag21Aa(:89-100). - Why it's built this way: the mismatched-passwords fact submits once and asserts the field-level validation text rather than re-clicking, because the
[Compare]validation firesOnInvalidSubmitand a re-clicking helper would make the message flicker out from under the wait; the text is asserted (not the alert element) because it renders in both the Server-mode and WebAssembly paths, while the page-level alert appears only on the Server-mode prerender path (:43-47,:60-64). - Where it's used: subclassed in both consumer E2E suites (
MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5,MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserRegistrationTests.cs:5).
BunitInteractionExtensions
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:12· Level 0 · static class
- What it is: a small set of intention-revealing helpers over bUnit's element API so a component test can say "click the button labelled Save" instead of hand-rolling a DOM query, deliberately preferring accessible text over brittle CSS-path selectors (
MMCA.Common.Testing.UI/Infrastructure/BunitInteractionExtensions.cs:7-11). - Depends on: no first-party types. Three externals: AngleSharp's
IElement(the DOM node type bUnit returns,:1), bUnit'sIRenderedComponent<TComponent>(:2), andMicrosoft.AspNetCore.Components.IComponentas the generic constraint (:3). See primer §3 for the external stack. - Concept introduced,
extension(T)blocks used for something other than DI. Everywhere else in this codebase the C# preview extension-member syntax registers services (primer §4); here the same feature bolts query and interaction members onto a third-party generic type. The declaration isextension<TComponent>(IRenderedComponent<TComponent> cut) where TComponent : IComponent(:14-15), so the extension block is itself generic over the component type and the members inside read as instance methods on the rendered component (cut.ClickButtonByText("Save")).[Rubric §28, Front-End Testing]assesses whether UI behavior is exercised through realistic automated checks; querying by the text a user actually sees is what keeps a component test asserting on behavior rather than on markup structure.[Rubric §15, Best Practices & Code Quality]assesses how well the code resists churn; a CSS-path selector breaks on any wrapper change, while a text query survives it. - Walkthrough: three members, all inside the single extension block.
FindButtonByText(string text)(:18-25) materializes every<button>viacut.FindAll("button")(:20), then takes the first whoseTextContentcontainstextunderStringComparison.OrdinalIgnoreCase(:21). On no match it throws anInvalidOperationExceptionwhose message enumerates the trimmed text of every button present, pipe-separated (:22-24), so a failing test names the buttons it could see instead of reporting a bare null.ClickButtonByText(string text)(:28-29) is the action form: it delegates toFindButtonByTextand calls bUnit'sClick(), which raises the component'sonclickthrough the renderer.HasText(string text)(:32-33) is the read form, a case-insensitiveContainsovercut.Markup. It answers on the whole rendered markup, not only visible text, which is the trade-off for having no dependency beyond the rendered string.
- Why it's built this way: the diagnostic message is the point. bUnit's own
Find(selector)throws with the selector, which tells you nothing when the button simply rendered with different copy; listing the actual buttons turns a red test into a readable one on the first run. - Where it's used: by the shared UI's own page tests, for example
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationSendTests.cs:99,125,362(the send and cancel buttons of the notification composer) andMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Notifications/NotificationInboxTests.cs, and across the ADC and Store component suites that derive from BunitComponentTestBase. - Caveats / not-in-source:
HasTextmatches attribute values and element names too, since it searches raw markup; a test asserting on user-visible copy that happens to collide with a CSS class name would pass for the wrong reason.
CapturedRequest
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:189· Level 0 · sealed record
- What it is: the immutable snapshot of one HTTP request a UI service sent, recorded by CapturingHttpMessageHandler so a test can assert on what went out, not only on what came back (
MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:185-188). - Depends on: BCL only,
System.Net.Http.HttpMethod,System.Uri, andIReadOnlyDictionary<string, string>. No first-party types. - Concept introduced, capture-then-assert on the outbound side. A canned-response fake proves the service handles a given payload; it says nothing about whether the service built the right URL, attached the bearer token, sent the right
If-Match, or serialized the right body. Recording every request as a value object makes those assertions possible without a server.[Rubric §14, Testability]assesses how much of a component's contract can be verified in isolation; capturing the request turns the client's outbound contract (route, query string, headers, body) into something a unit test can pin.[Rubric §9, API and Contract Design]applies indirectly: these captures are where a UI service's assumed route and header shape is asserted against the API's actual one. - Walkthrough: six positional members plus one init member.
- The positional six (
:159-164) are filled by the handler'sCaptureAsync(:82-88).Methodis the verb;Uriis the full request URI and is nullable becauseHttpRequestMessage.RequestUriis;Pathisuri?.AbsolutePathwith an empty-string fallback (:85);PathAndQuerykeeps the query string thatPathdrops (:86), which is what a paging or filter assertion needs;Authorizationis the header rendered back to a string, or null when absent (:87);Bodyis the content read as text, left null when the request had no content (:75-79). Headers(:179-180) is anIReadOnlyDictionary<string, string>over anOrdinalIgnoreCasecomparer, defaulting to an empty dictionary and set through an object initializer byCaptureAsync(:89-91). It flattens request headers and content headers into one lookup, which is what makes headers the service layer sets itself assertable.- The XML doc records why
Headersis a non-positional init member rather than a seventh positional parameter (:172-177): adding one would rewrite the record's primary constructor andDeconstruct, breaking every consumer that constructs or deconstructs a captured request.Authorizationtherefore stays where it is even though the same value is also reachable throughHeaders.
- The positional six (
- Why it's built this way: being a record, structural equality and a readable
ToString()come for free, which is what makes a failing assertion legible. The deliberate refusal to moveAuthorizationintoHeadersis a source-compatibility decision written into the type rather than left implicit. - Where it's used: exposed as the ordered
IReadOnlyList<CapturedRequest>on CapturingHttpMessageHandler.Requests(:42) and filtered by itsRequestsFor(method, absolutePath)(:61-65); consumed by every UI HTTP-service test in the three repos, for example the optimistic-concurrency assertioncaptured.Headers["If-Match"].Should().Be("\"v7\"")and the content-header pair inMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:208-210,226,229.
ErrorSummaryExtensions
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:10· Level 0 · static class
- What it is: a reader that pulls the shared
ErrorSummarycomponent's current messages out of a rendered component under test, so a form test asserts on the validation text a user would see rather than on raw markup (MMCA.Common.Testing.UI/Infrastructure/ErrorSummaryExtensions.cs:6-9). - Depends on: no first-party types at compile time, but it is coupled by contract to
MMCA.Common.UI'sErrorSummary.razor, which renders the marker class on its title (MMCA.Common/Source/Presentation/MMCA.Common.UI/Components/Forms/ErrorSummary.razor:13). Externals: bUnit'sIRenderedComponent<TComponent>and AngleSharp's element query API, plusMicrosoft.AspNetCore.Components.IComponentas the constraint (:1-2). - Concept introduced, testing a component that renders two different shapes for the same state.
ErrorSummaryrenders several messages as a<ul>of<li>items but a single message as plain text inside the alert (ErrorSummary.razor:15-29). A test that only queriedliwould read an empty summary exactly when one rule is broken, which is the most common form-validation case, and the failure would look like the component not rendering at all. This helper reads both shapes and strips the alert's title so the caller gets one list either way; the remark on the member says so directly (ErrorSummaryExtensions.cs:22-26).[Rubric §24, Forms/Validation/UX Safety]assesses whether validation feedback is coherent and verified; this is the assertion primitive that makes the form suites able to check it.[Rubric §28, Front-End Testing]is the capability it serves.[Rubric §21, Accessibility]applies obliquely: the two rendering shapes exist because several independent failures must be announced as several list items, and the reader has to understand that choice to see through it. - Walkthrough: one constant and one extension member.
ErrorSummaryTitleClass(:13) is the publicconst"mmca-error-summary-title", the class the component puts on its title and therefore the marker that distinguishes an error summary'sMudAlertfrom any other alert on the page. Exposing it as a constant lets a consumer's own assertion use the same literal.- The
extension<TComponent>(IRenderedComponent<TComponent> cut)block (:15-16) mirrors the shape BunitInteractionExtensions introduces. ErrorSummaryMessages()(:27-47) guards the rendered component (:29), then finds the first.mud-alertthat contains an element carrying the title class (:31-32). No such alert means no summary is showing, and it returns an empty list (:33-36). It then querieslidescendants: when any exist, it returns their trimmed text one entry per rule (:38-42). Otherwise it takes the single-message shape, reads the title's text and removes that substring from the alert's wholeTextContentordinally, trims, and returns either an empty list or that one message (:44-46).
- Why it's built this way: matching the alert by the title class rather than by position keeps the reader working on a page that renders other
MudAlertinstances (an info banner, an offline notice). Stripping the title by string removal rather than by DOM surgery keeps the helper read-only against the rendered tree. - Where it's used: covered directly by
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/Forms/ErrorSummaryExtensionsTests.cs:22,33,43,54, which pins all four branches (no summary, single-message shape, list shape, and an alert without the title class), and used by the ADC create-form suites, for exampleMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Events/EventCreateTests.cs:55,.../Question/QuestionCreateTests.cs:57, and.../Activity/ActivityCreateTests.cs:91. - Caveats / not-in-source: the single-message branch removes the title text by ordinal string replacement, so a message whose text happens to contain the title string would have that fragment removed as well.
FieldErrorAccessibilityExtensions
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:12· Level 0 · static class
- What it is: a bUnit assertion that a form field in an invalid state is exposed to a screen-reader user: one call,
ShouldExposeFieldErrorAccessibly(inputId, expectedMessage), on a rendered component (MMCA.Common.Testing.UI/Infrastructure/FieldErrorAccessibilityExtensions.cs:14-26). - Depends on: no first-party types. Externals: bUnit's
IRenderedComponent<TComponent>with theIComponentconstraint (:14-15), AngleSharp element queries throughFindAll, and FluentAssertions. - Concept introduced, asserting an accessibility contract rather than markup shape. Three facts together make a validation error usable without sight: an element with
role="alert"is rendered, the input carriesaria-invalid="true", and an id listed in itsaria-describedbypoints at an element whose text contains the message. Each is easy to break independently (a renamed id leavesaria-describedbydangling while the visible text still looks right), so the helper checks all three and gives each assertion abecause:reason that names the failing fact.[Rubric §21, Accessibility]assesses whether accessibility is verified; this is the unit-tier check that field-level errors are announced.[Rubric §24, Forms/Validation/UX Safety]assesses coherent validation feedback; this pins that the feedback is associated with its field.[Rubric §28, Front-End Testing]is the capability it serves, and it complements ErrorSummaryExtensions, which reads the summary text. - Walkthrough: one extension member inside an
extension<TComponent>(IRenderedComponent<TComponent> cut)block (:14-15), the same shape as BunitInteractionExtensions.- Guards:
cutnon-null,inputIdandexpectedMessagenot null or whitespace (:28-30). - Alert: at least one
[role="alert"]element exists (:32-33). - Input: exactly one element carries
id="{inputId}"(:35-36), and itsaria-invalidequals"true"(:38-40). - Description:
aria-describedbyis split on spaces and must be non-empty (:42-45); every referenced id is resolved withFindAll, and the resolved elements' text must be non-empty, so a dangling reference fails (:47-52); finally at least one description containsexpectedMessageby ordinal comparison (:53-55).
- Guards:
- Why it's built this way: resolving the referenced ids instead of only checking the attribute is present is what catches a dangling
aria-describedby; a presence-only check would pass on a reference to nothing. - Where it's used: no call sites outside the defining file in the current tree (the brief records none); the bUnit tier it belongs to is described in ADR-103.
FreshApiClientFactory
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:73· Level 0 · sealed class
- What it is: an
IHttpClientFactorytest double that returns a brand-newHttpClienton everyCreateClientcall, whatever name is asked for (in practice"APIClient"), all wired to one shared handler with a fixed base address (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:66-72). - Depends on:
System.Net.Http.IHttpClientFactoryandHttpMessageHandler(BCL). Constructed with a CapturingHttpMessageHandler in practice, by UiHttpServiceHarness (:47) and by HttpTestDoubles.ClientFactory(MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:23-24). - Concept introduced, a fake whose lifetime semantics are load-bearing. The shared UI services acquire a client per call and dispose it afterwards, so a factory double that caches one instance would hand the second call a disposed client and fail with an
ObjectDisposedExceptionthat looks like a product bug. The XML doc states exactly that ("A fresh instance per call is load-bearing",:69-71).[Rubric §14, Testability]assesses whether test doubles reproduce the real collaborator's contract; matchingIHttpClientFactory's ownership semantics, not just its signature, is the difference between a double that works and one that misleads. - Walkthrough: a primary-constructor class taking
(HttpMessageHandler handler, Uri baseAddress)(:73) with one member.CreateClient(string name)(:79-80) ignores the name and returnsnew HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress }. ThedisposeHandler: falseflag is the second load-bearing detail: it lets the handler outlive each client, so the recorded CapturedRequest list survives across calls and the harness alone owns handler disposal (:63). - Why it's built this way: ignoring the client name keeps the double usable from any repo without knowing the named-client key the service under test resolves, while the shared handler keeps one capture log per test.
- Where it's used: exposed as UiHttpServiceHarness
.ClientFactory(:47,58) and returned from HttpTestDoubles.ClientFactory(HttpTestDoubles.cs:23-24); the fresh-instance contract and the shared base address are asserted directly inMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38.
MarkupSnapshotResult
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:110· Level 0 · readonly record struct
- What it is: the two-field outcome of a MarkupSnapshot comparison,
IsMatchplus a human-readableMessage(MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:107-110). - Depends on: nothing. That is the entire design point.
- Concept introduced, returning a result instead of asserting. A shipped test-infrastructure package that called an assertion API would drag an assertion library into every consumer's dependency graph and pin them to its version. Returning a value and letting the caller assert keeps the package dependency-free, which the class doc states explicitly ("kept dependency-free so the shipped package pulls in no assertion library",
:11-12). This mirrors the Result pattern the product code uses for expected failures, applied here to a test helper.[Rubric §32, Dependency and Supply-Chain]assesses how carefully the dependency surface of shipped packages is managed; areadonly record structwith two fields adds nothing to a consumer's transitive closure.[Rubric §28, Front-End Testing]is the capability this serves. - Walkthrough: a positional
readonly record structwithbool IsMatch(true when the markup matched the committed baseline or was just refreshed) andstring Message(:102-104).readonlyplusstructmeans no allocation per comparison; the record shape gives value equality and a printable form. Callers use it asresult.IsMatch.Should().BeTrue(result.Message), so the diff text that MarkupSnapshot.BuildDiffMessageproduced becomes the assertion failure message. - Where it's used: returned from every branch of MarkupSnapshot
.Match(:45,51-53,58-59) and asserted by the golden-markup regressions inMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61and by the ADC component snapshot suites.
MudProviderHandles
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:168· Level 0 · protected sealed nested record
- What it is: the three handles returned by BunitComponentTestBase
.RenderMudProviders(), one per rendered MudBlazor provider, so a test can query the popover, dialog, or snackbar markup after triggering it (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:167). - Depends on: bUnit's
IRenderedComponent<T>and MudBlazor'sMudPopoverProvider,MudDialogProvider, andMudSnackbarProvider(:157-160, imports at:2,11-12). - Concept: the provider-root problem, taught with BunitComponentTestBase. MudBlazor renders overlays into provider components that normally live in the app layout; in a component test there is no layout, so a dialog or snackbar has nowhere to go. Rendering the providers as separate roots gives them somewhere, and this record is how the test keeps a reference to each root to query it afterwards.
[Rubric §20, Design System and Theming]assesses how the shared component library is adopted and exercised; overlay behavior is only testable once the design system's provider contract is honored in the test host. - Walkthrough: a positional record with
Popover,Dialog, andSnackbar(:157-160), constructed once inRenderMudProviders()after the threeRender<T>()calls (:150-153).Dialogis the handle a test queries forMudMessageBoxconfirm buttons;Snackbaris the one it queries for toast text. - Where it's used: returned by
RenderMudProviders()(:148-154). Most callers discard the return value and only need the providers rendered, for exampleMMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCarts/ShoppingCartListTests.cs:75,90,105,119; a test that must click inside the dialog keeps it, as inMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/UserListTests.cs:349,357-359, which waits for the confirm button to appear inproviders.Dialogand clicks it.
MutableAuthenticationStateProvider
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:173· Level 0 · private sealed nested class
- What it is: the
AuthenticationStateProviderBunitComponentTestBase registers, holding one principal that the test can swap at any point, with change notification to listeners. - Depends on:
Microsoft.AspNetCore.Components.Authorization(AuthenticationStateProvider,AuthenticationState,:5) and BCLClaimsPrincipal. - Concept introduced, the two ways a Blazor page learns who the user is. Some components read the cascading
AuthenticationStatevalue; others injectAuthenticationStateProviderand callGetAuthenticationStateAsync()themselves. A hardcoded-anonymous provider serves neither well once a test needs the signed-in branch, so the base supplies a mutable one and drives both routes from the same principal. The class remark says exactly this: the provider is mutable because it is "a superset of a hardcoded-anonymous one" (:24-27). This provider is the only auth double left in the harness: the authorization decision itself is made by the framework's realDefaultAuthorizationService(:62), which evaluates whatever principal this provider holds, so the principal (and its role claims) is the whole of what a test controls.[Rubric §19, State Management]assesses how shared client state is owned and propagated; auth state is the canonical cascading state, and this is the test-side owner of it. - Walkthrough: a primary-constructor class taking the initial principal (
:167), storing it in a mutable_principalfield (:169).SetPrincipal(ClaimsPrincipal)(:171-175) assigns the field and then calls the baseNotifyAuthenticationStateChangedwith a freshAuthenticationState, so an already-rendered<AuthorizeView>re-evaluates rather than keeping its first answer.GetAuthenticationStateAsync()(:177-178) returnsTask.FromResult(new AuthenticationState(_principal)), so no async state machine is allocated per call. - Where it's used: instantiated as the base's
_authProviderfield seeded withAnonymous(:42), registered as a singletonAuthenticationStateProvider(:63), and mutated bySetUser(:126) and by everyRenderAscall before rendering (:140). It has no usage outside the defining file, being a private nested type.
Route
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:170· Level 0 · private sealed nested record
- What it is: one registered canned response inside CapturingHttpMessageHandler: an HTTP method, an absolute path, a status code, an optional JSON body, and the method that turns all four into a fresh
HttpResponseMessage. - Depends on: BCL
HttpMethod,HttpStatusCode,HttpResponseMessage,StringContent, andSystem.Text.Encoding(MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:1-2). No first-party types. - Concept introduced, why the canned response is a recipe rather than an object.
HttpContentis consumed when read, so handing the sameHttpResponseMessageto two calls fails the second one, and the shared UI services run behind a retry pipeline that can legitimately send the same request twice. Storing the body as a string and rebuilding the response per request removes that whole class of false failure; the handler's own doc calls this out ("Responses are built fresh per request so a Polly retry pipeline never reuses a consumedHttpContent",:14-16).[Rubric §29, Resilience and Business Continuity]assesses how retry and recovery behavior is handled; a fake that cannot be retried would make the retry pipeline itself untestable. - Walkthrough: a positional record
(HttpMethod Method, string Path, HttpStatusCode StatusCode, string? JsonBody)(:139) with one method.ToResponse()(:141-150) news up anHttpResponseMessage(StatusCode)and, only whenJsonBodyis not null, attaches aStringContent(JsonBody, Encoding.UTF8, "application/json")(:144-147), so a body-less status (204, 404) round-trips as a genuinely empty response rather than as an empty JSON document. - Where it's used: appended by
SetResponse(:57) and selected by the handler'sRespondusingLastOrDefaulton method plus case-insensitive path (:124-125), which is what makes "last registration wins" true and lets a test override an earlier canned route mid-test. - Caveats / not-in-source: the match ignores the query string entirely (only
AbsolutePathis compared,:121-125), so two routes that differ only by query parameters cannot be registered separately. Query assertions go through CapturedRequest.PathAndQueryinstead.
CapturingHttpMessageHandler
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:20· Level 1 · sealed class
- What it is: the canned-response, request-recording
HttpMessageHandlerthat lets an HTTP-backed UI service be unit tested with no server: it answers every request from registered routes or a responder delegate, and records what was sent (MMCA.Common.Testing.UI/Infrastructure/CapturingHttpMessageHandler.cs:7-19). - Depends on: its two companions Route (private nested) and CapturedRequest. Externals:
System.Net.Http.HttpMessageHandler(the BCL extension point),System.Text.Jsonfor body serialization, andSystem.Net/System.Text(:1-3). - Concept introduced, faking at the transport boundary instead of the service boundary. The alternative would be mocking the UI service's own interface, which proves nothing about the service. Subclassing
HttpMessageHandlerputs the fake one layer lower, so the service under test runs its real URL construction, its real serialization, its real header setting, its real status-code handling, and its real error mapping; only the wire is simulated.[Rubric §14, Testability]assesses how much real behavior a unit test covers; this is the difference between testing the service and testing a mock of it.[Rubric §9, API and Contract Design]applies because the registered routes are a written-down expectation of the API's shape, and[Rubric §12, Performance and Scalability]indirectly: the whole tier runs in-process with no sockets, which is what keeps the UI test tier in the fast unit run. - Walkthrough: state, then the two configuration modes, then the send path.
- Static
WebJson(:22) is a singleJsonSerializerOptions(JsonSerializerDefaults.Web)instance, matching what the WebAPI actually emits (camelCase, case-insensitive reads), so a body serialized here deserializes in the service exactly as a real response would. _respond(:24) is the optional responder delegate;_routes(:25) and_requests(:26) are the registration and capture lists, and_sync(:27) is the oneSystem.Threading.Lockthat guards both.- The parameterless constructor (
:33-35) selects route-registration mode; the delegate constructor (:41) selects responder mode. The two are not exclusive: routes always win, and the delegate is the fallback (:144-168). Requests(:44-53) returns a snapshot of the captures, in order, as anIReadOnlyList<CapturedRequest>: it copies_requestsinto a new collection under the lock (:48-51), so a test that enumerates it while requests are still arriving cannot hit a "collection was modified" failure.SetResponse(method, absolutePath, statusCode, body = null)(:60-72) normalizes the body through a switch: null stays null (empty body), astringis treated as raw JSON and passed through untouched, and anything else is serialized withWebJson(:62-67). It then appends a Route under the lock (:68-71).RequestsFor(method, absolutePath)(:75-85) filters the captures by verb and case-insensitive path via a collection expression built under the lock, the assertion helper most tests use.SendAsync(:87-96) is the override: capture first, then respond. The capture is awaited outside the lock and only the_requests.Addis taken under it (:89-93), so noawaitever runs while the lock is held.CaptureAsync(:98-117) reads the request content to a string when present (:100-104) and builds the CapturedRequest from the URI, absolute path, path-and-query, and the Authorization header rendered to a string (:106-113), then sets itsHeadersfromCaptureHeaders(:114-116). Reading the body here, before responding, is what makes the body assertable at all, since the content stream is consumed by the read.CaptureHeaders(:124-142) builds oneOrdinalIgnoreCasedictionary, copying every request header (:128-131) and then, when a body is present, every content header (:133-139), comma-joining multi-valued entries. Its doc gives the reason both halves are needed:If-Matchlives on the request whileContent-Typelives on the content, and a caller asserting either should not have to know which (:119-123).Respond(:144-168) resolves the response in strict precedence: the last matching registered route, looked up under the lock (:146-160), else the responder delegate (:162-165), else404 Not Foundwith an empty body (:167). The doc explains the 404 default as deliberate, mirroring the WebAPI's not-found behavior so an incidental call (a token refresh, say) does not have to be set up in every test (:12-14).
- Static
- Why it's built this way: the two configuration modes serve two different test shapes. A service test that exercises one endpoint many ways wants the delegate ("answer everything this way"); a test that walks a multi-call flow wants named routes with last-registration-wins overrides. Supporting both in one handler avoids two parallel fakes drifting apart. The lock exists because UI services fan out: the class doc states that recording and route registration are thread-safe "so a service that fans requests out concurrently loses none", and that
Requestsreturns a snapshot (:17-18). A plainList<T>appended from parallelSendAsynccalls can drop entries or throw, which would turn a correctTask.WhenAllin a service into a flaky test. - Where it's used: owned by UiHttpServiceHarness (
MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:24,39,43-45), constructed directly by tests that wire the pieces themselves (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:21,35), and used across the ADC UI service suites (for exampleMMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Services/SessionLive/LivePollUIServiceTests.csandMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Services/SpeakerDashboardServiceTests.cs). It is covered in its own right byMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/CapturingHttpMessageHandlerTests.cs:25,37,53,65, includingCapture_RecordsEveryRequest_WhenRequestsArriveConcurrently(CapturingHttpMessageHandlerTests.cs:252), which drives parallel workers through one handler withTask.WhenAll(:267) to pin the concurrency guarantee. - Caveats / not-in-source: the lock covers the two lists only. The responder delegate (
:164) is invoked outside it, so a delegate that keeps its own state across calls must be thread-safe itself if the service under test issues requests concurrently.
MarkupSnapshot
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:22· Level 1 · static partial class
- What it is: a dependency-free golden-markup regression helper. It takes a component's rendered markup, normalizes the parts that change per render, compares it to a committed baseline
.htmlfile next to the calling test, and returns the outcome for the caller to assert on (MMCA.Common.Testing.UI/Infrastructure/MarkupSnapshot.cs:6-22). - Depends on: MarkupSnapshotResult as its return type. Externals:
System.Text.RegularExpressionswith source-generated regexes andSystem.Runtime.CompilerServices.CallerFilePathAttribute(:1-2), plus BCL file I/O. - Concept introduced, snapshot testing without screenshots. A pixel screenshot baseline is OS-dependent and needs per-platform golden management; normalized markup is deterministic and identical on every CI runner, which is why this comparison can live in the fast in-solution unit tier rather than in a browser job. The class doc states both halves of that trade (
:13-19). Two policies make it safe to rely on: a baseline is refreshed only whenUPDATE_SNAPSHOTS=1is set, and both a refreshing run and a missing baseline are reported as a non-match, so a regression can never pass silently, neither on an absent snapshot nor on a CI job or shell that leaves the variable set.[Rubric §28, Front-End Testing]assesses whether UI regressions are caught automatically; this is the structural-regression half of that (axe scans in the E2E tier are the accessibility half).[Rubric §20, Design System and Theming]assesses how the shared component library is governed; snapshotting the shared primitives is what makes an unintended change to a reused component fail somebody's build. - Walkthrough:
Match(markup, snapshotName, [CallerFilePath] callerFilePath = "")(:31-65) is the whole public surface. It guards both string arguments (:33-34), normalizes the markup (:36), and locates the baseline asSnapshots/{snapshotName}.htmlin the directory of the calling test file, creating the folder if needed (:37-39).[CallerFilePath]is the mechanism: the compiler bakes the caller's source path in, so baselines live beside the tests that own them with no configuration.- Three outcomes follow. With
UPDATE_SNAPSHOTS=1(compared ordinally,:41) it writes the file and returns a non-match whose message says the baseline was refreshed and must be reviewed and committed (:42-51). That is deliberate (comment:44-46): a run that regenerates baselines must never count as a pass, or a CI job that leaves the variable set would turn every markup regression green while rewriting the baselines. With no existing baseline it likewise writes the file and returns a non-match telling you to review and commit it (:53-59). Otherwise it reads the baseline, normalizes its line endings and trims (:61), and returns match or a built diff (:62-64). Normalize(markup)(:69-75) does the work that makes the comparison stable: CRLF to LF and trim (:71), then replace both GUID forms with the literal{guid}(:72-73), then right-trim every line (:74). MudBlazor injects fresh GUIDs into element ids and ARIA associations on every render, so without this step no snapshot would ever match twice.BuildDiffMessage(:77-97) walks both line arrays to the first difference and reports the line number with the expected and actual text plus theUPDATE_SNAPSHOTS=1instruction (:89-91); if no line differs it reports a length mismatch (:95-96).GuidRegex(:99-100) andHex32Regex(:102-103) are[GeneratedRegex]partial properties, which is why the class ispartial: the patterns compile at build time rather than being interpreted per call.
- Why it's built this way: writing the baseline but still failing the test is the deliberate part, on first run and on a refresh alike. Writing and passing would let a regression bless itself on any machine where the file happened to be missing, or where
UPDATE_SNAPSHOTS=1was left set; failing but not writing would make creating a new snapshot a manual chore. Writing plus failing gives the file for free and forces a human to review and commit it, then re-run without the variable. - Where it's used: by
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Components/PrimitivesSnapshotTests.cs:21,31,41,51,61, which snapshots the shared UI primitives (EmptyState,PageHeader,PageErrorState,PageLoadingState) against baselines committed under that project'sSnapshots/folder, and by the per-module ADC component snapshot suites, for exampleMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Components/ComponentsSnapshotTests.cs:69,87,124,143,MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/ComponentsSnapshotTests.cs:122,135,147,160,175,197, andMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:33,43. - Caveats / not-in-source: the normalizer replaces any bare 32-character hex token, so a legitimate markup value of that shape (a content hash in an asset URL, for example) would also collapse to
{guid}and stop being compared. Some ADC suites wrap the markup in a repo-localStable(...)helper before callingMatch, so the shipped normalizer is not always the only pass applied.
StubTokenStorageService
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:13· Level 1 · sealed class
- What it is: the canned ITokenStorageService for UI HTTP-service tests. It returns fixed tokens with no platform storage behind it, lets the access-token read be swapped for a throwing delegate, and mutates its values on set and clear so login and logout flows are assertable (
MMCA.Common.Testing.UI/Infrastructure/StubTokenStorageService.cs:5-12). - Depends on: ITokenStorageService from
MMCA.Common.UI.Services.Auth(:1), the contract the real per-head storage implementations satisfy. That reference is one of the reasons the Testing.UI package carries aProjectReferencetoMMCA.Common.UI, as its comment records (MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:28-30). - Concept introduced, a stub with a failure switch. Most stubs only model the happy path, which leaves the interesting branch (what the service does when storage is unreachable) untested. Blazor has a real version of that branch: during prerender, JS interop is unavailable, so a browser-storage-backed implementation throws. Exposing the read as a replaceable delegate lets a test reproduce that exact failure without a browser.
[Rubric §26, Front-End Security]assesses how tokens are stored and attached; the stub is what lets the bearer-attachment path be asserted at all.[Rubric §29, Resilience and Business Continuity]assesses how failure paths are handled; the swappable delegate is the extension point for testing them. - Walkthrough:
- The constructor takes
accessToken = "test-token"andrefreshToken = "test-refresh-token"(:18-23), assigns both, and setsAccessTokenProviderto a closure returning the propertyAccessToken(:22), not the constructor argument, so later mutations are visible through the default provider. AccessTokenandRefreshToken(:26,29) are mutable auto-properties, readable by a test as post-condition assertions.AccessTokenProvider(:36) is the swap point: replace it with a delegate that throwsInvalidOperationExceptionandGetAccessTokenAsyncreproduces the prerender storage failure (:31-35).- The four interface members follow:
GetAccessTokenAsync()invokes the provider (:39),GetRefreshTokenAsync()returns the property (:42),SetTokensAsync(access, refresh)assigns both and completes (:45-50), andClearTokensAsync()nulls both (:53-58).
- The constructor takes
- Why it's built this way: routing the read through a delegate while set and clear mutate plain properties keeps the common case zero-ceremony (construct it and go) and the failure case one assignment away, with no separate throwing subclass to keep in sync.
- Where it's used: constructed by UiHttpServiceHarness and exposed as its
TokenStorageproperty (MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:48,61), returned from HttpTestDoubles.TokenStorage(MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:28-29), and covered directly byMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/StubTokenStorageServiceTests.cs:19,28,38,51,65,76.
TestPrincipal
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:7· Level 1 · static class
- What it is: the factory for the
ClaimsPrincipalinstances component tests render as, with the claim shape the shared pages actually read (MMCA.Common.Testing.UI/Infrastructure/TestPrincipal.cs:6). - Depends on: AuthClaimTypes from
MMCA.Common.Shared.Auth(:2), for thesubclaim name. Externals: BCLSystem.Security.Claims(:1). - Concept introduced, the authenticated-identity trap. A
ClaimsIdentityis authenticated only when it was constructed with an authentication type; an identity built from claims alone reportsIsAuthenticated == falseand every<AuthorizeView>renders its anonymous branch, which is a confusing way for a test to fail. This factory always passes one (authenticationType: "TestAuth",:31), and the base class'sAnonymousprincipal deliberately omits it (BunitComponentTestBase.cs:41), so the two states are produced by the same mechanism from opposite ends.[Rubric §11, Security]assesses how identity and claims are modelled; encoding the app-wide identifier claim names in one shared factory keeps every UI test honest to the claims the product code reads. - Concept, the dual-written user identifier. The identifier is written twice, under AuthClaimTypes
.Subject(which is"sub",MMCA.Common/Source/Core/MMCA.Common.Shared/Auth/AuthClaimTypes.cs:34) and underClaimTypes.NameIdentifier(TestPrincipal.cs:27-28), because a real principal reaches a page under either name: a token read directly carries the rawsubthe token service emits, while the JWT bearer handler maps it ontoNameIdentifier. A page reading through ClaimsPrincipalExtensions.FindUserIdValueresolves both, and writing both here also keeps a page that still reads a single claim type working under this double; the remark says exactly that (:14-21). - Walkthrough: two members.
AuthenticatedUser(string userId = "1", string name = "Test User", params string[] roles)(:22-32) builds a claim list withClaimTypes.Name, thesubclaim, andClaimTypes.NameIdentifier(:24-29), appends oneClaimTypes.Roleclaim per supplied role (:30), and returns aClaimsPrincipalover aClaimsIdentitycarrying"TestAuth"(:31).InRole(string role, string userId = "1")(:90-91) is a named convenience over the first: an authenticated"Test User"carrying exactly one role, the role named by the caller. The framework declares no role vocabulary of its own, so a test states the role its own app uses (:84-85).
- Why it's built this way: the test host evaluates authorization for real (
BunitComponentTestBaseregistersDefaultAuthorizationService,MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:59-63), so a view's role requirement is checked against the principal's claims, and getting the role claims right on the principal is what makes an authorized-branch assertion meaningful. - Where it's used: passed to BunitComponentTestBase
.RenderAsacross all three repos, for exampleMMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/HappeningNow/HappeningNowTests.cs,MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Common/EventFilteredListPageBaseTests.cs,MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/NavMenuTests.cs:54(a named user), andMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/SessionsTests.cs:86. - Caveats / not-in-source: the framework ships no built-in role names (the earlier
Organizerconvenience was removed in favor ofInRole); every caller, in Common, ADC, or Store, states its own app's role string throughInRoleor throughAuthenticatedUser(roles: ...)directly.
BunitComponentTestBase
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:38· Level 2 · public abstract class
What it is: the one shared base class for bUnit component tests across all three repos. It stands up MudBlazor services and the vendor-neutral UI facades over them, puts JS interop in loose mode, wires the real authorization service over a mutable authentication-state provider, registers localization and a clock, and adds the helpers derived tests actually call (
RenderUnderTest,RenderAs,RenderMudProviders,SetUser,ConfigureDataGridListPageHost) (MMCA.Common.Testing.UI/Infrastructure/BunitComponentTestBase.cs:17-37).Depends on: its two nested types MutableAuthenticationStateProvider and MudProviderHandles, plus TestPrincipal as the intended source of authenticated principals. It also registers first-party UI services: the
AddCommonUiFacades()pair fromMMCA.Common.UI(:53, defined atMMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:238) and the two list-page state services ListPageStateService and ListPageQueryStateService (:110-111). Externals: ASP.NET Core'sDefaultAuthorizationServiceandIAuthorizationServicefromMicrosoft.AspNetCore.Authorization(:3), bUnit'sBunitContextbase,JSRuntimeMode,AddBunitPersistentComponentState, andSetRendererInfo(:2), MudBlazor'sAddMudServices, the three providers, andIBrowserViewportService(:11-12),Moqfor the inert viewport double (:10), andMicrosoft.Extensions.DependencyInjectionplus itsTryAddextensions (:6-7).Concept introduced, the component test harness. A Blazor component under test needs a renderer, a service provider, and whatever ambient services its markup injects; assembling that per test project is how three repos end up with three subtly different harnesses. This base assembles it once, in a shipped package, so a component that renders in Common's tests renders identically in ADC's and Store's. Several decisions carry the weight:
- The vendor-neutral facades ship with the harness (
:53): every migrated page and the sharedResulthelpers injectIToastServiceand the confirm-dialog facade.AddCommonUiFacades()wraps the MudBlazor services registered on the line above, so it belongs immediately afterAddMudServices(); without it a consumer's component test fails to resolve IToastService and each repo re-registers the same pair in its own base. The registration usesTryAddinternally, so a test wanting a recording double registers one afterwards and last registration wins (:48-52). - Loose JS interop (
:55): MudBlazor components probe JS during render (measurements, popover positioning). In bUnit's default strict mode every unplanned call throws, so a test would have to pre-plan interop it does not care about; loose mode returns defaults instead. - Real authorization, test-controlled principal (
:56-63):AddAuthorizationCore()registers the genuine Blazor authorization plumbing, and the base then registers the framework's ownDefaultAuthorizationServiceas the singletonIAuthorizationService(:62) next to the mutable provider instance (:63). The comment gives two reasons for the explicit registration (:58-61): bUnit pre-registers a placeholderIAuthorizationServicethat throws, andAddAuthorizationCoreonlyTryAdds, so without it the placeholder would win; and the real service evaluates the requirements a view actually builds (Roles, the default deny-anonymous policy), whereas a permissive double would authorize every authenticated principal whatever the view asks for.<AuthorizeView>therefore runs its real code path, role checks included, and the only substituted input is the principal.[Rubric §11, Security]is the reason this matters: a role-gated branch that renders for any signed-in user in tests would hide exactly the authorization bug the test exists to catch. - Localization by default (
:65-69): the comment ties this to ADR-027; components injectIStringLocalizer<T>, so registering the open generic once lets every component test render localized markup against the neutral resources in the component's own assembly with no per-test setup.[Rubric §27, i18n]assesses whether localization is systematic rather than incidental; making it the harness default is what keeps localized pages testable everywhere. - A clock, added with
TryAdd(:71-75):TimeProvider.Systembacks the staleness measurements the notification bell's poll interval and the navigation-refresh window depend on.TryAddSingletonmeans a test that drives time deterministically registers a fake provider of its own and wins. The comment also notes thatAddLoggingabove already brought in the options infrastructure, soIOptions<T>of an unconfigured settings class resolves to its defaults. - The viewer time zone, added with
TryAddScoped(:81): ViewerTimeZone is the service the notification and session pages format instants with. Under loose JS interop the browser read returns null, so pages render in UTC unless a test sets up thegetTimeZonecall itself (:78-80). - Anonymous by default (
:40,42): the seed principal has no authentication type, so tests opt in to the authorized branch explicitly rather than inheriting it.
[Rubric §28, Front-End Testing]assesses whether UI behavior is verified automatically; this base is the entry point for the entire component tier.[Rubric §14, Testability]assesses how cheaply a unit renders in isolation.[Rubric §15, Best Practices & Code Quality]: the class remark documents that the bUnit v2 symbols (BunitContext,Render<T>) are confined to this file, so a hypothetical downgrade to bUnit v1 changes this one class and no derived test, because derived tests only ever callRenderUnderTest/RenderAs(:28-35).- The vendor-neutral facades ship with the harness (
Walkthrough: fields first, then the constructor, then the helpers.
Anonymous(:40) is astatic readonly ClaimsPrincipalover a bareClaimsIdentity, unauthenticated precisely because no authentication type is supplied._authProvider(:42) is the MutableAuthenticationStateProvider seeded with it.- The constructor (
:45-82) runs the registrations described above, in order:AddMudServices()(:46),AddCommonUiFacades()(:53), looseJSInterop.Mode(:55),AddAuthorizationCore()(:56), the singletonDefaultAuthorizationService(:62), the singleton provider instance (:63),AddLogging()andAddLocalization()(:68-69),TryAddSingleton(TimeProvider.System)(:76), andTryAddScoped<ViewerTimeZone>()(:81). ConfigureDataGridListPageHost(isInteractive = true, rendererName = "Server", stubViewport = true)(:105-123) is the one helper whose call ordering is load-bearing, and the reason it exists as a single method. It registers the two list-page state services withTryAddScoped(:110-111), substitutes an inertMock.Of<IBrowserViewportService>()whenstubViewportis true (:113-116), callsAddBunitPersistentComponentState()because the list-page base persists grid state across the prerender/interactive boundary (:118-119), and finally callsSetRendererInfo(new RendererInfo(rendererName, isInteractive))(:121-122). The doc spells out the trap:SetRendererInfobuilds and freezes the bUnit service provider, so any registration made after it is silently ignored and the page resolves the framework default instead of the test's double; fifteen call sites across the repos hand-rolled this block and each had to remember that rule (:82-88). Two parameters encode real branches:isInteractivebecause list pages bound their prerender fetches onRendererInfo.IsInteractive, so a non-interactive renderer means no data ever loads (:98-102), andstubViewportbecause in bUnit no browser answers MudBlazor's breakpoint subscription, so the real service would leave the card-versus-grid choice up to timing (:89-96). The viewport double is registered with a plainAddrather thanTryAddon purpose:AddMudServicesalready registered the real implementation and last registration wins.[Rubric §22, Responsive/Cross-Browser]assesses whether responsive behavior is deterministic and verified; pinning the viewport is what makes the mobile-versus-desktop branch of a list page assertable at all.SetUser(ClaimsPrincipal)(:126) forwards to_authProvider.SetPrincipal, changing the injected provider's answer and notifying listeners without rendering a new root, which is how a mid-test sign-in or sign-out is simulated.RenderUnderTest<TComponent>(parameters)(:129-132) is the anonymous-render entry point and simply callsRenderAs(Anonymous, parameters).RenderAs<TComponent>(principal, parameters)(:135-146) is the one that matters: it sets the provider's principal (:140) and then renders with a cascadingTask<AuthenticationState>value added ahead of the caller's parameters (:143-144). Driving both routes from one principal is what makes the cascading consumers and the injecting consumers agree.RenderMudProviders()(:153-159) rendersMudPopoverProvider,MudDialogProvider, andMudSnackbarProvideras separate roots and returns the MudProviderHandles triple. The doc is explicit that it must be called before the component under test (:148-152).
Why it's built this way: registering the framework's own
DefaultAuthorizationServiceplus a concrete provider instance, rather than mocks or a permissive decision double, means the authorization pipeline under test is the production one; only the principal is substituted, so a test that renders a role-gated view as a principal lacking the role sees the denied branch, as production would (:58-62).Moqis a deliberate dependency for exactly one thing, the viewport double, because a hand-written stub would have to track every member MudBlazor adds toIBrowserViewportService(MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:17-21). And keeping the version-specific bUnit symbols in one file is a blast-radius decision recorded in the class remark rather than left to be rediscovered.Where it's used: subclassed by a thin repo-local
BunitTestBasein each consumer, which adds only that repo's extra registrations:MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/BunitTestBase.cs:17(theme service, culture applier, capability fallbacks, public-link builder) andMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/BunitTestBase.cs:24(the ADR-042 device-capability defaults, the public-link builder, the public session schedule service, and an emptyIConfiguration), plus the equivalent bases in ADC's Identity and Engagement suites and in Store's Sales, Catalog, and Identity UI test projects.ConfigureDataGridListPageHostis called from every list-page suite, for exampleMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Common/DataGridListPageBaseTests.cs:35,MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.UI.Tests/Pages/ShoppingCarts/ShoppingCartListTests.cs:38,MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/SessionListEventFilterTests.cs:41(stubViewport: false), andMMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Common/EventFilteredListPagePrerenderTests.cs:42(isInteractive: false, the prerender branch). The base's own registration contract is guarded byMMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/BunitComponentTestBaseFacadeTests.cs:18, which derives from the shipped base rather than the repo-local one precisely so the facade registrations cannot be papered over by a consumer.Caveats / not-in-source: the package does reference the shared UI:
MMCA.Common.Testing.UI/MMCA.Common.Testing.UI.csproj:30has aProjectReferencetoMMCA.Common.UI, and the base now uses it for both the facades and the list-page state services. The harness is therefore not UI-agnostic, and a consumer cannot take the bUnit base without also takingMMCA.Common.UI.
UiHttpServiceHarness
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:12· Level 2 · sealed class (IDisposable)
- What it is: the one-line setup for a UI HTTP-service test. It owns the disposable plumbing every such test needs (the capturing handler, a fresh-client-per-call factory, and a token storage stub) so the test constructs one object and hands its parts to the service under test (
MMCA.Common.Testing.UI/Infrastructure/UiHttpServiceHarness.cs:3-11). - Depends on: CapturingHttpMessageHandler, FreshApiClientFactory, and StubTokenStorageService, all first-party in this package. Externals:
IHttpClientFactoryandIDisposable(BCL). - Concept introduced, the aggregate fixture over the three-piece boundary. Assembling the same three collaborators by hand in each repo is exactly how the fresh-client rule (see FreshApiClientFactory) gets forgotten in one repo and not another. Collecting them behind one constructor makes the correct wiring the default, and gives one
Disposefor the one thing that actually needs it.[Rubric §14, Testability]assesses how cheap a correct test setup is; two lines of setup is the practical bar this clears.[Rubric §15, Best Practices & Code Quality]: with the wiring shipped in a package, a fix to the plumbing reaches ADC, Store, and Common in one version bump rather than three edits. - Walkthrough:
DefaultBaseAddress(:15) is astatic readonly Uriofhttps://gateway.test/, applied to every created client so services can build relative URIs the way they do against a real gateway.- Two public constructors mirror the handler's two modes and both funnel into one private constructor. The route-registration one (
:23-26) takesaccessToken = "test-token"and an optional base address and builds a route-registration handler; the responder one (:35-41) takes the delegate and builds a responder-mode handler. - The private constructor (
:43-49) is where the wiring lives: store the handler, resolve the base address againstDefaultBaseAddress(:46), build the FreshApiClientFactory over that handler and address (:47), and build the StubTokenStorageService with the canned token (:48). - Four read-only properties expose the parts:
Handler(:52) forSetResponseregistration and request assertions,BaseAddress(:55),ClientFactory(:58) typed asIHttpClientFactoryto hand to the service, andTokenStorage(:61) typed concretely so a test can reachAccessTokenProvider. Dispose()(:63) disposes only the handler. The clients are the caller's to dispose, which is consistent withdisposeHandler: falseon each created client, so the capture log outlives them.
- Why it's built this way: passing
accessToken: nullproduces an anonymous harness in one argument, which is how the "no bearer header when signed out" case is tested without a second fixture type. And keeping the two public constructors as thin forwarders means the mode choice is visible at the call site while the wiring exists once. - Where it's used: the default setup for the HTTP-backed UI service tests in all three repos.
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/UiHttpServiceHarnessTests.cs:19,33,45,48,56,67-68,78covers the harness itself, including both modes, the default and custom base addresses, and the with-token versus anonymous pair.
HttpTestDoubles
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI·MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:12· Level 3 · static class
- What it is: the a-la-carte counterpart to UiHttpServiceHarness: standalone factory helpers for tests that wire the pieces individually, plus the canned-response builders both styles share (
MMCA.Common.Testing.UI/Infrastructure/HttpTestDoubles.cs:7-11). - Depends on: UiHttpServiceHarness (for the shared default base address), FreshApiClientFactory, StubTokenStorageService, and ITokenStorageService as the returned contract (
:3). Externals:System.Net.Http.Json.JsonContentandSystem.Net.HttpStatusCode(:1-2). - Concept introduced, canned responses that match the real API's failure shape. The response builders are not generic conveniences:
ProblemResponseemits the{ title, detail }body the WebAPI actually returns for a domain failure, so the UI-side error mapping under test sees the shape it will see in production (:40-51). A fake that returned a bare 400 would let the mapping pass while being wrong. Its doc names the consumer on the other side, ProblemDetailsResultReader, and records what it does with this shape: the plain-ProblemDetails body becomes a single error coded"Http.{status}"typed from the status code, and a test that needs the individual error codes preserved sends anerrorsarray throughJsonResponse<T>instead (:41-46).[Rubric §9, API and Contract Design]assesses whether error contracts are consistent and honored on both sides; this helper is where the UI tier's assumption about the API's error body is written down.[Rubric §29, Resilience, Reliability & Business Continuity]applies because that ProblemDetails shape is produced by shared middleware rather than by any one endpoint. - Walkthrough: five members, all static.
BaseAddress(:15) is aliased to UiHttpServiceHarness.DefaultBaseAddress, so both wiring styles share one origin and a test can compare across them.ClientFactory(handler, baseAddress = null)(:23-24) returns a FreshApiClientFactory over the given handler, defaulting the address toBaseAddress.TokenStorage(accessToken = "test-token")(:28-29) returns a StubTokenStorageService typed as the interface; pass null for an anonymous client.JsonResponse<T>(payload, statusCode = OK)(:33-34) builds a response whose content isJsonContent.Create(payload), which serializes with web defaults, matching what the WebAPI sends.EmptyResponse(statusCode = NoContent)(:37-38) builds a body-less response, the 204 shape a command endpoint returns.ProblemResponse(detail, title = "Domain Exception", statusCode = BadRequest)(:47-51) delegates toJsonResponsewith an anonymous{ title, detail }object.
- Why it's built this way: the harness is the right default, but a test that needs two handlers, or a handler shared with a non-HTTP collaborator, would have to fight it. Keeping the same primitives available individually means neither style forks its own copies, and the response builders stay identical across both.
- Where it's used: by the responder-delegate style of service test across the consumer UI suites, and covered directly by
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Infrastructure/SharedHttpTestDoublesTests.cs:22,27-28,38,47-49,56,67,75-76,86,97, which pins the shared base address, the fresh-client contract, the three token-storage cases, and all three response builders including the custom-status ProblemDetails form. - Caveats / not-in-source: the responses these builders return are single-use
HttpResponseMessageinstances. Handing one to a responder delegate that returns the same instance twice would fail on a retry, which is why the delegate is invoked per request (see Route) and why these helpers are normally called inside the delegate rather than before it.
RoleAdminEditPageTestsBase<TPage>
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI.Pages·MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminEditPageTestsBase.cs:22· Level 4 · class
- What it is: a shared
BunitComponentTestBasesubclass that drives any consumer's single-role permission editor page through the same four facts, so ADC and Common each supply only their own page type and role/permission names rather than re-writing the suite. - Depends on:
BunitComponentTestBasefor the render harness;IRoleAdminUIService, mocked, as the page's client;RolePermissionsResponseandPermissionCatalogResponseas the response shapes the mock returns. - Concept: a generic
TestsBase<TPage>that a consumer closes over its own concrete page type, so the suite runs against ADC'sRoleEditand Common's ownRoleAdminEditPageTestssample page without duplication.[Rubric §28, Front-End Testing](assesses whether component behavior is pinned rather than only rendered smoke-tested): the base pins that a compiled-in permission renders ticked and disabled and an editable one does not (lines 76-89), and that the one write this page performs submits the role's complete stored set rather than a diff (lines 95-112). - Walkthrough: the constructor registers the mocked
Rolesclient (line 28); five abstract members (EditedRole,OtherRole,CompiledPermission,StoredPermission,RosterRoute, lines 34-52) let the consumer supply its own names, plus a virtualRoleParameterNamedefaulting to"Role"(line 55) for a page whose route parameter is spelled differently.TheRouteRole_ReachesTheComponentAndItsCatalogRenders(line 58) asserts two permission inputs render.ACompiledPermission_IsLocked_AndAnEditableOneIsNot(line 76) targets the checkbox's own<input data-permission>rather than a wrapper, because MudCheckBox splats unmatched attributes straight onto that element (lines 82-83).TickingAPermissionAndSaving_SubmitsItAsTheStoredSet(line 96) changes one checkbox, clicks save, and verifies the client received exactly the one ticked permission as the stored list (lines 106-111).ThePage_LinksBackToTheRoleRoster(line 119) asserts an anchor to the consumer'sRosterRoute.RenderEditor(line 134) is the shared render entry point, andSetupDefaultClient(line 142) stubs all three service calls: it runs at render rather than in the constructor because the role and permission names are abstract members a constructor cannot call before the derived class initializes (lines 140-141), and everyResult-returning setup is explicit because an unstubbed one hands back null rather than a success. - Why it's built this way: a permission the host compiles in is locked because removing it is a code change, not a data edit, and the server refuses to store it either way (lines 71-73), the ownership split recorded in ADR-058 that keeps the compiled and stored permission sets from being confused.
- Where it's used:
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Administration/RoleAdminEditPageTests.csandMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Roles/RoleEditTests.cs, each a thin subclass supplying its own page type and role/permission constants.
RoleAdminListPageTestsBase<TPage>
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI.Pages·MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/RoleAdminListPageTestsBase.cs:21· Level 4 · class
- What it is: the companion base for the role roster page that RoleAdminEditPageTestsBase<TPage> edits into, pinning that every listed role renders its compiled/stored counts and an edit link, and that a failed load hides the roster rather than showing an empty one.
- Depends on:
BunitComponentTestBase;IRoleAdminUIService, mocked;RolePermissionsResponse. - Concept: shares the generic-base pattern with its edit-page sibling.
[Rubric §28, Front-End Testing]:WhenTheFetchFails_RendersNoRoleRows(line 234) is the load-bearing fact here, because a failed fetch renders zero rows, which is visually identical to a genuinely empty roster (lines 229-231), so the page's shared inline error state has to take over instead of the empty-roster message. - Walkthrough: the constructor registers the mocked
Rolesclient (line 182); two abstract role names (lines 187-191) and an abstractEditRoute(string)(line 250) let the consumer supply its own routing.RendersOneRowPerRoleWithItsCompiledAndStoredCounts(line 194) asserts two[data-testid=compiled-count]and two[data-testid=stored-count]cells.EachRow_LinksToTheRoleEditorRoute(line 216) asserts the edit-link hrefs equalEditRouteapplied to each role in order, because the edit link is the one required parameter the component takes, so a page that stopped passing it would not compile, but a page that passed the wrong shape would compile and dead-end the operator (lines 210-214).SetupRoles(line 258) and the privateSetupDefaultRoles(line 265) stubGetAllAsync; the default roster fixture gives the first role two compiled permissions and no stored ones and the second one compiled permission plus a stored permission already granted elsewhere (lines 266-270), enough variety to distinguish the two counts. - Why it's built this way: like its edit-page sibling, roles and permission names are supplied per consumer through abstract members rather than hardcoded, so the same fact set runs against ADC's and Common's own role vocabularies without copying the test bodies (ADR-058).
- Where it's used:
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Administration/RoleAdminListPageTests.csandMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Roles/RoleListTests.cs.
ConfirmEmailPageTestsBase
MMCA.Common.Testing.UI ·
MMCA.Common.Testing.UI.Pages·MMCA.Common/Source/Hosting/MMCA.Common.Testing.UI/Pages/ConfirmEmailPageTestsBase.cs:26· Level 5 · class
- What it is: a shared
BunitComponentTestBasesubclass pinning the email-confirmation page's link-driven confirm flow and its resend affordance, so both apps'ConfirmEmailpages run the same seven facts. - Depends on:
BunitComponentTestBase;IEmailConfirmationUIService, mocked; the framework's ConfirmEmail page component it renders;NavigationManager's[SupplyParameterFromQuery]routing. - Concept:
[Rubric §28, Front-End Testing]: the constructor pre-stubs both service calls to succeed (lines 299-304) so each fact only overrides the one call it needs to fail, andRenderPage(line 422) routes throughNavigationManager.GetUriWithQueryParametersrather than passing component parameters directly, because bUnit cannot pass a[SupplyParameterFromQuery]value as a component parameter and must instead navigate exactly as the emailed link's browser click would (lines 414-418). - Walkthrough:
WithACompleteLink_ConfirmsOnceAndReportsSuccess(line 314) confirms on the initial render, forces a second render, and verifies the confirmation call still ran exactly once, so a re-render after success does not spend the single-use token a second time (lines 320-321).WithARefusedToken_ShowsTheApisMessage_AndKeepsTheFormWithAResend(line 328) stubs a refused-token error and asserts the API's own message surfaces alongside a resend affordance and no success marker.WithNoToken_DoesNotCallTheServiceAndShowsTheForm(line 352) asserts a link with nothing to redeem lands on manual entry with no service call and no error, because posting a request that can only be refused would be pointless (lines 347-349).SubmittingWithoutAToken_ExplainsWhatIsMissingAndDoesNotCallTheService(line 368) covers the manual-submit path to the same rule.Resend_AsksForAFreshLinkAndSaysSoWithoutConfirmingTheAccountExists(line 386) asserts the resend notice says "unconfirmed account" rather than promising a delivered email, because the endpoint answers 202 either way (lines 381-383).ResendWithNoAddress_DoesNotCallTheService(line 401) guards the same anti-enumeration posture when the address itself is missing.Email(line 311) is virtual, defaulting to"ada@example.com", so a consumer can override it to an app-shaped address. - Why it's built this way: the single-use-token and anti-enumeration behaviors are shared server-side contracts (an unauthenticated confirm and an unauthenticated resend), so pinning them once here keeps ADC's and Common's pages from drifting apart on the one screen a mis-click on an old link actually reaches (ADR-058).
- Where it's used:
MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Pages/Auth/ConfirmEmailPageTests.csandMMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Pages/Users/ConfirmEmailTests.cs.
SampleGridRow
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Pages·MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:9· Level 0 · record (sealed, public)
- What it is: the row DTO behind the virtualized-grid gallery page. It is deliberately plain, because the point of
/gridis the windowing behaviour of DataGridListPageBase<TDto>, not the shape of the data (MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:5-8). - Depends on: nothing first-party. Five BCL positional members only.
- Concept introduced: cross-reference the DTO and manual-mapping conventions taught in 00-primer.md. What is worth noting here is the absence of the usual machinery: no identifier-type alias, no validator, no mapper, no audit fields. A test-fixture DTO exists to give the component under test something to bind, so it carries exactly the column types the grid needs to exercise, one integer, two strings, a
DateTime, and adecimal.[Rubric §14, Testability]assesses how cheaply a component can be driven in isolation; a five-member positional record is the smallest possible stand-in for a real paged API contract. - Walkthrough: one line.
public sealed record SampleGridRow(int Id, string Name, string Category, DateTime CreatedOn, decimal Amount)(:9). The member set is chosen to cover the fourPropertyColumnformatting paths the page binds: a plain integer, ordinal strings, aDateTimerendered withFormat="yyyy-MM-dd", and adecimalrendered withFormat="F2"(MMCA.Common.UI.Gallery/Pages/GridGallery.razor:28-32). It ispublicrather thaninternal(unlike every stub in this unit) because it is the generic argument of the page's base class,@inherits DataGridListPageBase<SampleGridRow>(MMCA.Common.UI.Gallery/Pages/GridGallery.razor:4). - Where it's used: the element type of SampleGridData
.All(MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:25), theTof the page'sMudDataGrid(MMCA.Common.UI.Gallery/Pages/GridGallery.razor:17), and, through the page, the payload GridPageE2ETests asserts against by naming the exact rendered value"Row 0001"(MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:39,:55,:80).
GalleryFakeAuthenticationHandler
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21· Level 1 · class (sealed, internal)
- What it is: a cookie-toggled fake ASP.NET Core authentication handler for the backend-less gallery. A request carrying
gallery_auth=1authenticates as a fixed "Gallery Visitor" principal that also holds thenotifications:managepermission claim; every other request stays anonymous (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:30-47). - Depends on:
Microsoft.AspNetCore.Authentication.AuthenticationHandler<AuthenticationSchemeOptions>(the abstract base), the base constructor tripleIOptionsMonitor<AuthenticationSchemeOptions>/ILoggerFactory/System.Text.Encodings.Web.UrlEncoder(MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:21-25), and BCLSystem.Security.Claims. Two first-party names from the Shared layer supply the permission claim:AuthClaimTypes.Permission(using MMCA.Common.Shared.Auth,:5) andNotificationPermissions.Manage(using MMCA.Common.Shared.Notifications,:6), used at:42. - Concept introduced, the backend-less gallery stub pattern and its one non-inert member. The gallery host (GalleryHost) renders the real
MMCA.Common.UIcomponents with no live API behind them, so every consumer-supplied extension point the shared UI expects is replaced by a benign stub. This type is the exception that proves the rule: it is not inert, because the shared notification and sessions pages carry a real[Authorize].MapRazorComponentssurfaces that attribute as endpoint metadata, and the authorization middleware then needs a genuine authentication scheme registered (without one it throws) and a genuine authenticated principal (without one the pages redirect to/logininstead of rendering for the scan). The doc comment records exactly that reasoning (MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:10-20). Rather than removing the guard for testability, the gallery supplies a real scheme whose only decision input is a cookie.[Rubric §28, Front-End Testing]assesses whether the UI has real-browser render and accessibility coverage; toggling sign-in per test with a cookie is what lets one host scan both the anonymous chrome (/login,/register,/components,/grid) and the signed-in guarded pages.[Rubric §11, Security]assesses how authentication is implemented; note the deliberate inversion here, the handler trusts an unsigned cookie value, acceptable only because this assembly is unpackaged test infrastructure (the doc comment closes with "never copy into a real host",MMCA.Common.UI.Gallery/Stubs/GalleryFakeAuthenticationHandler.cs:18-19). - Walkthrough: two internal constants pin the contract shared with the host and the tests,
SchemeName = "GalleryFake"(:27) andCookieName = "gallery_auth"(:28).HandleAuthenticateAsync()(:30) is the single override. It short-circuits first: whenRequest.Cookies[CookieName]is not exactly"1"it returnsAuthenticateResult.NoResult()(:32-35), which means "this scheme has no opinion", leaving the request anonymous rather than failing it. Otherwise it builds aClaimsIdentitywith two claims, aClaimTypes.Nameof"Gallery Visitor"and anAuthClaimTypes.Permissionclaim whose value isNotificationPermissions.Manage(:39-43), and, critically, passesSchemeNameas the authentication type (:44): supplying an authentication type is what makesIdentity.IsAuthenticatedtrue. The permission claim exists because the notification history and compose pages gate onnotifications:manage, as does their navigation entry, so the signed-in scan reaches the pages rather than an Access Denied view (comment,:37-38). It wraps that principal in anAuthenticationTicketand returnsAuthenticateResult.Success(:45-46). Everything is synchronous throughTask.FromResult, so no I/O occurs. - Why it's built this way: keeping the guard real and faking only the credential means the E2E scan exercises the same authorization pipeline the deployed hosts run, so an accidental loss of
[Authorize]on the shared notification or sessions pages cannot be papered over by a permissive test host. - Where it's used: registered as the default (and only) authentication scheme by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:72-75), followed byAddAuthorization()(MMCA.Common.UI.Gallery/GalleryHost.cs:76) and theUseAuthentication()/UseAuthorization()middleware pair (MMCA.Common.UI.Gallery/GalleryHost.cs:119-120). The cookie is seeded on the Playwright browser context by NotificationPagesE2ETests.SeedSignedInCookieAsync(MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:90-99) before each of its five guarded-page scans (:23,:38,:54,:67,:80), by SessionsPageE2ETests for the devices page (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:23,:39-40), and by MobileTopRowE2ETests for its signed-in top-row check (MMCA.Common.UI.E2E.Tests/Layout/MobileTopRowE2ETests.cs:93-97).
NullTokenRefresher
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:9· Level 1 · class (sealed, internal)
- What it is: an ITokenRefresher that never has a session to refresh. The gallery has no API to refresh against; the stub exists only so the DI graph stays complete (
MMCA.Common.UI.Gallery/Stubs/NullTokenRefresher.cs:5-8). - Depends on: ITokenRefresher from
MMCA.Common.UI.Services.Auth. Nothing else. - Concept introduced: the purest instance of the stub pattern introduced in GalleryFakeAuthenticationHandler, collapsing an outbound extension point to a constant.
[Rubric §14, Testability]assesses how cheaply a component renders in isolation; reducing token refresh to a constant is what keeps the scan from ever reaching a token endpoint. - Walkthrough: one member.
AcquireAccessTokenAsync(CancellationToken = default)(:11-12) is an expression body returningTask.FromResult<string?>(null), which is the interface's own documented "no valid session exists" answer (MMCA.Common.UI/Services/Auth/Tokens/ITokenRefresher.cs:19-24), so any caller takes its null-token path. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:64) in the pre-AddUISharedstub block. In the shared UI the only constructor taking anITokenRefresherisAuthUIService(MMCA.Common.UI/Services/Auth/AuthUIService.cs:44-47), which the gallery replaces with NoOpAuthUIService, so this registration is graph-completeness insurance rather than a live collaborator.
NullTokenStorageService
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:11· Level 1 · class (sealed, internal)
- What it is: an in-memory-empty ITokenStorageService: there is no stored session in the gallery. It exists so the AuthDelegatingHandler that
AddUISharedregisters resolves cleanly, and it is never actually invoked because the gallery makes no API calls (MMCA.Common.UI.Gallery/Stubs/NullTokenStorageService.cs:6-10). - Depends on: ITokenStorageService from
MMCA.Common.UI.Services.Auth. - Concept introduced: the same stub pattern as NullTokenRefresher, covering storage rather than refresh, and the first place where a stub exists purely to satisfy a transitive constructor.
AddUISharedregisters AuthDelegatingHandler as transient (MMCA.Common.UI/DependencyInjection.cs:122) and attaches it to the named"APIClient"pipeline (MMCA.Common.UI/DependencyInjection.cs:126,:101), and that handler's primary constructor takes anITokenStorageService(MMCA.Common.UI/Services/Auth/AuthDelegatingHandler.cs:15-16). Nothing inAddUISharedsupplies one, so the host must.[Rubric §26, Front-End Security]assesses how auth tokens are stored and handled; this stub deliberately holds nothing, so the test host persists no credential material at all, not even in memory. - Walkthrough: four members, each inert but shape-complete so DI binds.
GetAccessTokenAsync()(:12) andGetRefreshTokenAsync()(:14) returnTask.FromResult<string?>(null);SetTokensAsync(accessToken, refreshToken)(:16) andClearTokensAsync()(:18) discard their inputs and returnTask.CompletedTask. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:63), inside the block placed ahead ofAddUIShared(MMCA.Common.UI.Gallery/GalleryHost.cs:58-66). - Caveats / not-in-source: unlike NoOpAuthUIService, this registration is not a
TryAddoverride.AddUISharedmakes noITokenStorageServiceregistration anywhere in its body (MMCA.Common.UI/DependencyInjection.cs:40-213); the concrete storage services are supplied per host head instead, so in the gallery this stub is the only registration rather than a winning one.
SampleGridData
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Pages·MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:16· Level 1 · class (static, internal)
- What it is: the gallery's in-memory stand-in for a paged API. The rows are generated once from fixed inputs, with no randomness and no clock, so every E2E run, on every browser engine, scrolls over an identical data set and a row assertion can name an exact value (
MMCA.Common.UI.Gallery/Pages/SampleGridRow.cs:11-15). - Depends on: SampleGridRow and BCL
System.Globalization.CultureInfo. Nothing else. - Concept introduced, determinism as a precondition for real-browser assertions. The stubs above collapse an extension point to a constant; this one goes further and makes a whole data set a constant. A generated fixture is only usable in an E2E assertion if it produces byte-identical output on every run and every engine, so the builder takes its three inputs (index, a fixed five-item category array, a fixed UTC epoch) and derives everything else arithmetically. There is no
Random, noDateTime.UtcNow, and the one string is formatted withCultureInfo.InvariantCultureso a CI runner's locale cannot change it. That is what lets GridPageE2ETests assert on the literal text"Row 0001"rather than on a shape.[Rubric §28, Front-End Testing]assesses real-browser coverage of the UI; a deterministic 1,000-row set is what makes "the DOM holds a small fraction of the data" a falsifiable claim.[Rubric §23, Front-End Performance]assesses whether the front end bounds the work it does per render; this data set exists specifically so the virtualization window can be measured instead of asserted by inspection. - Walkthrough:
RowCount = 1000(:19) is a public constant, documented as "the row count the E2E windowing assertion is written against". The test mirrors it as its own privateDataSetRowCount = 1000(MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:19) rather than referencing it, since the test assembly does not see this internal type.Categories(:21) is a static readonly five-item array, andEpoch(:23) a static readonly2026-01-01T00:00:00Zwith an explicitDateTimeKind.Utc.All(:25) is anIReadOnlyList<SampleGridRow>auto-property initialized once fromBuild(), so generation happens a single time per process and every request serves the same instances.Build()(:27-42) fills aSampleGridRow[RowCount]. For indexiit computes a one-basedid(:32), formatsNameasRow {id:D4}throughstring.Create(CultureInfo.InvariantCulture, ...)(:35) giving the zero-padded"Row 0001"the test asserts on, cyclesCategorywithCategories[i % Categories.Length](:36), spacesCreatedOnone hour apart withEpoch.AddHours(i)(:37), and derivesAmountasid * 37 % 10000 / 100m(:38), an integer modulo followed by a decimal divide, which keeps every amount inside 0.00 to 99.99 with two decimal places.
- Why it's built this way: a virtualization test needs enough rows that rendering all of them would be obviously wrong, and few enough that generation costs nothing at startup. 1,000 rows against the test's
MaxRenderedRows = 200ceiling (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:26) gives a fifth-of-the-data-set margin, which the test's own comment explains is generous headroom over the roughly 50 rows a 70vh viewport at 52px per row plus MudBlazor overscan actually renders (MMCA.Common.UI.E2E.Tests/Layout/GridPageE2ETests.cs:21-25). - Where it's used:
MMCA.Common.UI.Gallery/Pages/GridGallery.razorreadsSampleGridData.RowCountfor its on-page caption (:12) andSampleGridData.Allinside its staticFetchPageAsync(:63), which sorts by the requested column and direction (:65-82), appliesSkip((pageNumber - 1) * pageSize).Take(pageSize)(:84), and returns aResult<(IReadOnlyList<SampleGridRow> Items, int TotalItems)>(:86-87). That delegate deliberately matchesIEntityService.GetPagedAsync's shape and 1-based page contract (comment,MMCA.Common.UI.Gallery/Pages/GridGallery.razor:52-53), so DataGridListPageBase<TDto>.LoadVirtualizedServerDataAsync(MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:790) maps the virtualization window onto it exactly as it would onto a real API.
GalleryUIModule
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:14· Level 3 · class (sealed, internal)
- What it is: a minimal IUIModule whose
Assemblyis the gallery itself, so the shared Blazor Router (Routes.razor, which scansUIModules.Select(m => m.Assembly)) discovers the gallery's own/componentsand/gridpages. Its nav links make the host browsable when run interactively (MMCA.Common.UI.Gallery/Stubs/GalleryUIModule.cs:9-13). - Depends on: IUIModule and NavItem from
MMCA.Common.UI.Common, SharedResource fromMMCA.Common.UI.Resources, plusMudBlazor.Iconsand BCLSystem.Reflection.Assembly. - Concept introduced: the UI-module contribution pattern (taught with IUIModule in the Common UI framework group) reused for a test host: a module contributes route-bearing assemblies and nav links to the shared shell.
[Rubric §18, UI Architecture]assesses how the front end composes independently-owned UI slices; the gallery participates in the exact module-discovery mechanism the real apps use, which is what makes the E2E evidence say something about that mechanism rather than about a bespoke test shell.[Rubric §25, Navigation & IA]: the four nav entries flow through the sameNavItemrecord the deployed apps' menus are built from, supplying its first four positional members (title key, href, icon, resource type) and leavingRequiredRole,RequiredClaim,Section, andGroupat their defaults (MMCA.Common.UI/Common/NavItem.cs:16).[Rubric §27, i18n]: passingtypeof(SharedResource)asTitleResourceopts the labels into the resource-key lookup ADR-027 defines, and because a key the resource type does not declare renders as the raw string, the untranslated gallery labels stay legible rather than blank (MMCA.Common.UI/Common/NavItem.cs:13-18). - Walkthrough:
NavItems(:16-22) is a collection-expressionIReadOnlyList<NavItem>of four entries, Login (/login), Register (/register), Components (/components), and Grid (/grid), each pairing a label, a route, a MudBlazor Material icon, andtypeof(SharedResource)(:18-21).Assembly(:24) is an expression-bodied property returningtypeof(GalleryUIModule).Assembly, so the Router additionally scans the gallery assembly for routable components, which is howMMCA.Common.UI.Gallery/Pages/ComponentsGallery.razor:1(@page "/components") andMMCA.Common.UI.Gallery/Pages/GridGallery.razor:1(@page "/grid") become reachable. The optionalIUIModulemembers the interface declares beyond these two are left at their defaults. - Where it's used: registered as a singleton
IUIModuleby GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:97); the pages it makes routable are what ComponentsPageE2ETests and GridPageE2ETests scan.
StubNotificationInboxUIService
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:11· Level 4 · class (sealed, internal)
- What it is: a canned INotificationInboxUIService returning fixed inbox data so NotificationBell and the notification inbox page render populated, real markup for the axe and render scans, with no backend (
MMCA.Common.UI.Gallery/Stubs/StubNotificationInboxUIService.cs:7-10). - Depends on: INotificationInboxUIService from
MMCA.Common.UI.Services.Notifications, the Result, PagedCollectionResult<T>, and PaginationMetadata types fromMMCA.Common.Shared.Abstractions, the UserNotificationDTO contract, and the solution-wideUserNotificationIdentifierTypealias. - Concept introduced: the stub pattern extended from inert no-ops to canned data. For components whose whole purpose is displaying content, an empty stub renders an empty (and therefore untested) tree, so this stub returns representative rows instead. Note that it still speaks the real Result contract on every member, so the pages take their success branches through the same unwrapping code the deployed apps run.
[Rubric §28, Front-End Testing]: populated markup is what lets axe evaluate contrast, roles, and the read/unread affordances against a realistic notification list rather than an empty state. - Walkthrough:
GetInboxAsync(pageNumber = 1, pageSize = 20, cancellationToken)(:13-14) builds a two-itemUserNotificationDTO[](:16-29): an unread "Welcome to MMCA" with a fixed UTCSentOnof 2026-01-02 09:00, and a read "Scheduled maintenance" carrying bothReadOnandSentOn, so the inbox exercises both visual states. It wraps them in aPagedCollectionResult<UserNotificationDTO>withnew PaginationMetadata(items.Length, pageSize, pageNumber)(:30-31), so the pager renders from real metadata rather than a hardcoded count.GetUnreadCountAsync()(:34-35) returns a successful constant3so the bell badge renders non-empty.MarkReadAsync(id, ct)(:37-38) andMarkAllReadAsync(ct)(:40-41) are no-ops returningResult.Success(), so the buttons are present and clickable and their handlers take the success path without any state change. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:82), alongside the scoped NotificationState (MMCA.Common.UI.Gallery/GalleryHost.cs:81). The scans that consume it are NotificationPagesE2ETests.NotificationInbox_Renders_AndHasNoWcag21AaViolations(MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:34-44) plus its two deep-link cases,.NotificationInboxDeepLink_ToAnUnknownNotification_DegradesToThePlainInbox(:47-62) and.NotificationInboxDeepLink_ToAKnownNotification_HighlightsThatCard(:64-75), each ending in an AxeOptions.Wcag21Aascan. - Caveats / not-in-source: the badge count
3(:34-35) is a hardcoded display value and does not reconcile with the two rowsGetInboxAsyncreturns; it exists to render a non-empty badge for the scan, not to be internally consistent.
StubPushNotificationUIService
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:11· Level 4 · class (sealed, internal)
- What it is: a canned IPushNotificationUIService so the notification history and compose pages render populated, real markup for the axe and render scans, with no backend (
MMCA.Common.UI.Gallery/Stubs/StubPushNotificationUIService.cs:7-10). - Depends on: IPushNotificationUIService from
MMCA.Common.UI.Services.Notifications, Result, PagedCollectionResult<T> and PaginationMetadata, plus the PushNotificationDTO and SendPushNotificationRequest contracts. - Concept introduced: the same canned-data variant of the stub pattern as StubNotificationInboxUIService, applied to the send and history side.
[Rubric §24, Forms/Validation/UX Safety]: the compose page is a form, and echoing the submittedTitleandBodyback in the returned DTO lets the render smoke reach the post-submit state without a real send. - Walkthrough:
SendAsync(request, cancellationToken)(:12-13) is an expression body returning a successfulPushNotificationDTOthat echoesrequest.Titleandrequest.Bodyand fixes the rest,Id = 99,SentByUserId = 1,RecipientCount = 42,Status = "Sent",CreatedOn2026-01-04 10:00 UTC (:14-17).GetHistoryAsync(pageNumber = 1, pageSize = 10, cancellationToken)(:19-20) builds a three-item history array (:23-45): one row withStatus = "Sent", one withStatus = "Failed", bothRecipientCount = 128, and a third withStatus = "Pending"andRecipientCount = 64added on purpose,NotificationListrenders the Warning-coloured chip for every status that is neither Sent nor Failed, and without a pending row the axe scan never reached the one chip whose palette colour was failing contrast (:37-39,:40-44), then wraps them in aPagedCollectionResult<PushNotificationDTO>withPaginationMetadata(:47-48). - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:83). The scans that consume it are NotificationPagesE2ETests.NotificationHistory_Renders_AndHasNoWcag21AaViolations(MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:20-32) and.NotificationCompose_Renders_AndHasNoWcag21AaViolations(:77-87); the history scan runs under AxeOptions.Wcag21AaExceptMudPagerComboboxbecause MudBlazor's pager combobox has no accessible name and is not fixable from app markup (MMCA.Common.UI.E2E.Tests/NotificationPagesE2ETests.cs:32).
StubRoleAdminUIService
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/StubRoleAdminUIService.cs:14· Level 4 · class (sealed, internal)
- What it is: a canned IRoleAdminUIService so the gallery's
/admin/rolesand/admin/roles/{role}hosts render the shared RoleAdminList and RoleAdminEdit components populated for the axe and render scans, with no backend (MMCA.Common.UI.Gallery/GalleryHost.cs:90-92). - Depends on: IRoleAdminUIService from
MMCA.Common.UI.Services.Administration, Result and Error, the RolePermissionsResponse and PermissionCatalogResponse contracts, and AdministrationPermissions for theManageRolesentry in the catalog. - Concept introduced: canned data with a tiny in-memory model behind it, one step beyond StubNotificationInboxUIService. A single
static readonly RolePermissionsResponse[](MMCA.Common.UI.Gallery/Stubs/StubRoleAdminUIService.cs:16-20) holds two roles,Admin(registeredorders:read,orders:write; storedreports:run) andMember(registeredorders:read; nothing stored), and every read member is derived from it, so the list, the editor and the catalog stay consistent with one another.[Rubric §28, Front-End Testing]: populated registered and stored permission sets are what let axe evaluate the real checkbox lists and edit affordances rather than an empty state. - Walkthrough:
GetAllAsync(:22-23) returns the array as a successfulResult.GetAsync(role)(:25-31) finds the role with an ordinal comparison and otherwise returnsResult.FailurewithError.NotFoundError("Authorization.RoleNotFound", "The role was not found."), so the editor's not-found branch is reachable.GetCatalogAsync(:33-36) returns aPermissionCatalogResponseof the role names plus the permission listorders:read,orders:write,reports:runandAdministrationPermissions.ManageRoles.SetStoredPermissionsAsync(role, permissions)(:38-45) looks up the role's registered permissions (empty when unknown) and echoes a newRolePermissionsResponse(role, compiled, permissions), without mutating the array, so a save takes the success path but nothing persists between requests. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:92). The scans that consume it are RoleAdminPagesE2ETests.RoleList_Renders_AndHasNoWcag21AaViolations(MMCA.Common.UI.E2E.Tests/Administration/RoleAdminPagesE2ETests.cs:22-31) and.RoleEditor_Renders_AndHasNoWcag21AaViolations(:34-43), each ending in an AxeOptions.Wcag21Aascan.
NoOpAuthUIService
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:14· Level 6 · class (sealed, internal)
- What it is: a no-op IAuthUIService for the backend-less gallery. The gallery renders the real Login, Register, and Sessions pages for accessibility and render-smoke scanning only, so every operation returns a benign default (
MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:8-13). - Depends on: IAuthUIService from
MMCA.Common.UI.Services.Auth, the Result and Error types fromMMCA.Common.Shared.Abstractions, and the LoginRequest, RegisterRequest, AuthenticationResponse, and RefreshSessionSummaryResponse contracts fromMMCA.Common.Shared.Auth. - Concept introduced, registration order as the override mechanism. This stub is registered before
AddUIShared, whoseTryAddScoped<IAuthUIService, AuthUIService>()(MMCA.Common.UI/DependencyInjection.cs:170) then defers to it, exactly as the class doc comment states (MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:11-12).TryAdd*is first-registration-wins, so a test host overrides only the extension points it names and inherits every other registration the shared UI makes, with no fork of the composition root.[Rubric §11, Security]assesses how authentication is handled; here the client-side auth boundary is neutralized entirely so the scan touches the real login and register markup without any credential flow.[Rubric §14, Testability]: substituting the top-level UI auth service for constants is what makes those pages renderable in isolation.[Rubric §1, SOLID]: the stub is only possible because the shared pages depend on theIAuthUIServiceabstraction rather than onAuthUIService, so swapping the implementation needs no change to any page. - Walkthrough:
- Shared constants (
:17-24): twostatic readonlysession GUIDs,CurrentDeviceSessionId(all-ones) andOtherDeviceSessionId(all-twos), built from their component parts rather than parsed, with a comment recording why, the MA0176 analyzer rejects parsing a constant at runtime (:16).Unavailable(:23-24) is a single sharedError.Failure("Gallery.AuthUnavailable", "The gallery has no backend.")reused by every failing member. - Credential operations (
:26-33):LoginAsync,RegisterAsync, andExchangeOAuthCodeAsynceach returnResult.Failure<AuthenticationResponse>(Unavailable), so the pages render and submit but never establish a session. - Session lifecycle (
:35-41):LogoutAsync()returnsTask.CompletedTask,TryRefreshTokenAsync(ct)returnsfalse, andChangePasswordAsync(currentPassword, newPassword, ct)returnsResult.Failure(Unavailable). - Password reset (
:45-49):RequestPasswordResetAsync(email, ct)andResetPasswordAsync(email, token, newPassword, ct)also fail withUnavailable. The comment at:43-44records why that still produces useful scan coverage, the Forgot Password page shows its confirmation regardless of the result (anti-enumeration), so the post-submit state renders anyway. ForgotPasswordPageE2ETests asserts exactly that (MMCA.Common.UI.E2E.Tests/Auth/ForgotPasswordPageE2ETests.cs:27-38). - Device sessions (
:54-75):GetSessionsAsync(ct)is the one member that returns real data. It builds twoRefreshSessionSummaryResponserows, a current device (all-ones id, a Chrome-on-Windows user agent,IpAddress: "203.0.113.7",IsCurrent: true) and another device (all-twos id, a Safari-on-iOS user agent, a nullIpAddress,IsCurrent: false). The comment at:51-53gives the reason, the axe scan has to reach the populated table, the current-device chip, and a live revoke button, which an empty state would hide, and one row is flagged current so both branches of the actions cell render. - Revoke (
:77-81):RevokeSessionAsync(sessionId, ct)(:77-78) andRevokeAllSessionsAsync(ct)(:80-81) both returnResult.Failure(Unavailable), so the per-row revoke buttons and the account-wide sign-out are present and wired without changing anything. The shared Sessions page shows a failed account-wide revoke and stays put rather than signing out locally (MMCA.Common.UI/Pages/Auth/Sessions.razor.cs:182-186,:191-194), so in the gallery that button keeps the page on its error path.
- Shared constants (
- Why it's built this way: placing the stub ahead of
AddUISharedexploits the shared UI'sTryAdd*idempotence rather than requiring the shared registration extension to grow test hooks, which keeps the production DI code free of test-only branches. Returning a real failureResultrather than a null also keeps the pages on their genuine error-handling paths, which is what makes an error alert's markup part of the scanned surface. - Where it's used: registered scoped by GalleryHost (
MMCA.Common.UI.Gallery/GalleryHost.cs:62), the first of the pre-AddUISharedstub registrations. It is consumed indirectly by LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, and SessionsPageE2ETests, the last of which asserts on the rendered form of these very rows, the visible texts "Chrome on Windows" and "This device" and the buttons "Sign out of Safari on iOS" and "Sign out of every device, including this one" (MMCA.Common.UI.E2E.Tests/Auth/SessionsPageE2ETests.cs:28-32). - Caveats / not-in-source: the two user-agent strings are supplied raw (
MMCA.Common.UI.Gallery/Stubs/NoOpAuthUIService.cs:63,:70) while the test asserts on friendly names. The parsing that turns one into the other lives in the shared sessions page, not in this stub or this unit.
GalleryAuthenticationStateProvider
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery.Stubs·MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:16· Level 8 · class (sealed, internal)
- What it is: the gallery's Blazor
AuthenticationStateProvider. It mirrors the request's authentication in both render phases, soAuthorizeViewandCascadingAuthenticationStateagree with whatever GalleryFakeAuthenticationHandler decided for the request. Without thegallery_authcookie both phases yield anonymous, preserving the deliberate signed-out chrome of the login, register, components, and grid scans (MMCA.Common.UI.Gallery/Stubs/GalleryAuthenticationStateProvider.cs:6-15). - Depends on:
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider(the abstract base) andIHostEnvironmentAuthenticationStateProvider(the interface the Blazor Server host calls into) at:16-17,IHttpContextAccessorinjected as a primary-constructor parameter (:16), and BCLClaimsPrincipal/ClaimsIdentity. - Concept introduced, the two render phases of interactive-server Blazor. A page first renders as static SSR inside the HTTP request, then, once the circuit connects, re-renders interactively over a WebSocket where there is no ambient
HttpContext. An auth-state provider therefore has to answer correctly in two different worlds. This class handles both: SSR reads the request user throughIHttpContextAccessor, and for the interactive circuit the framework pushes the handshake user in throughIHostEnvironmentAuthenticationStateProvider.SetAuthenticationState. The doc comment records that this replaced a former always-anonymous stub, which could not represent the signed-in state the guarded notification pages need (:7-9).[Rubric §19, State Management]assesses how client state is owned and propagated; auth state is the canonical cascading state, and this shows the two supply routes it has under interactive server rendering.[Rubric §28, Front-End Testing]: getting both phases right is what stops a guarded page from flipping to a signed-out tree mid-scan and producing a false pass on the wrong markup. - Walkthrough:
Anonymous(:19-20) is astatic readonly AuthenticationStatewrapping an emptyClaimsPrincipal(new ClaimsIdentity()), unauthenticated precisely because no authentication type is supplied (contrast GalleryFakeAuthenticationHandler, which passes one)._hostState(:22) is the nullable task the framework may have pushed in.GetAuthenticationStateAsync()(:24) checks_hostStatefirst and returns it verbatim when present (:26-29): the circuit's handshake user always wins. Otherwise it falls back to the SSR path, readinghttpContextAccessor.HttpContext?.User(:31) and returning a newAuthenticationState(user)only whenuser?.Identity?.IsAuthenticated == true, else the sharedAnonymous(:32-34). Both branches useTask.FromResult, so no async machinery is allocated per call.SetAuthenticationState(Task<AuthenticationState>)(:37-38) is theIHostEnvironmentAuthenticationStateProviderimplementation and simply stores the task; it does not callNotifyAuthenticationStateChanged. - Why it's built this way: the gallery mirrors rather than fabricates. Deriving the component-tree state from whatever the request actually authenticated as keeps one source of truth (the cookie) for the middleware, the endpoint authorization, and the render tree, so a scan cannot land in a split state where the endpoint admitted the request but the tree still renders signed-out.
- Where it's used: registered scoped as the
AuthenticationStateProviderby GalleryHost (MMCA.Common.UI.Gallery/GalleryHost.cs:66), immediately afterAddHttpContextAccessor()(MMCA.Common.UI.Gallery/GalleryHost.cs:65), which supplies its dependency. - Caveats / not-in-source: the class implements
IHostEnvironmentAuthenticationStateProviderbut is registered only under theAuthenticationStateProviderservice type (MMCA.Common.UI.Gallery/GalleryHost.cs:66). Whether the Blazor Server host resolves this same instance when it callsSetAuthenticationState, and at what point in the render sequence it does so, is framework behavior and is not determinable from this repository's source.
GalleryHost
MMCA.Common.UI.Gallery ·
MMCA.Common.UI.Gallery·MMCA.Common.UI.Gallery/GalleryHost.cs:24· Level 9 · class (public, static)
- What it is: a static builder that assembles the entire backend-less Blazor gallery host. It renders the real
MMCA.Common.UIauth pages (/login,/register,/profile/sessions), the shared notification pages, and the shared role administration pages (/admin/roles,/admin/roles/{role}), and the gallery's own primitives showcase (/components) and virtualized grid (/grid) against stub implementations of every consumer extension point, so a real-browser axe accessibility scan can run against the shared UI insideMMCA.Common's own CI (MMCA.Common.UI.Gallery/GalleryHost.cs:18-23). - Depends on: ASP.NET Core
WebApplication/WebApplicationBuilder, MudBlazor (AddMudServices,MMCA.Common.UI.Gallery/GalleryHost.cs:56), the sharedMMCA.Common.UIsurface (AddUIShared, the gallery's ownApproot component inMMCA.Common.UI.Gallery/Components/App.razor, andMMCA.Common.UI._Importsas the additional-assembly marker), and SupportedCultures fromMMCA.Common.Shared.Globalization. It wires in every stub in this unit: NoOpAuthUIService, NullTokenStorageService, NullTokenRefresher, GalleryAuthenticationStateProvider, GalleryFakeAuthenticationHandler, StubNotificationInboxUIService, StubPushNotificationUIService, StubRoleAdminUIService, and GalleryUIModule, plus the shared NotificationState and NullNotificationScopeProvider, and the shared ServiceWorkerEndpointExtensions (MapCommonServiceWorker,MMCA.Common/Source/Presentation/MMCA.Common.UI.Web/Pwa/ServiceWorkerEndpointExtensions.cs:60). - Concept introduced, a self-hostable test host as one buildable unit. The whole host build lives in
BuildApp(string[] args)(:31) rather than inProgram.cs, so two callers share the identical configured app: thedotnet runentry point, whichRunAsync()s it (MMCA.Common.UI.Gallery/Program.cs:7-8), and the E2E collection fixture GalleryHostFixture, whichStartAsync()s it on an ephemeral Kestrel port.[Rubric §28, Front-End Testing]assesses real-browser UI coverage; this host is the render target for CI'sui-e2ejob (MMCA.Common/.github/workflows/ci.yml:228), whose chromium, firefox, and webkit matrix legs (:236-237) are all required merge gates (:238-240).[Rubric §33, Developer Experience]:MMCA.Common.UI.Gallery/Program.cs:3-6records the rationale, oneBuildAppfor both entry points avoids the separatedotnet runplus health-poll that made ADC's e2e cold start fragile. - Walkthrough:
- Assembly name and base dir (
:36-37):typeof(GalleryHost).Assembly.GetName().Nameis captured without a null-forgiving operator; the comment at:33-35explains that CI's nullable analysis treatsAssemblyName.Nameas non-null and would flag!as an unnecessary suppression (IDE0370), and the value is only interpolated into a filename, which is null-safe either way. - Static web assets (
:48-51): the load-bearing fix. RCL_content/*CSS and JS plus_framework/blazor.web.jsresolve from the entry assembly's manifests and auto-load only in Development; when the E2E suite self-hosts in-process the entry assembly is the test host and the environment is Production, so neither default holds. The loader is pointed explicitly at{galleryAssemblyName}.staticwebassets.runtime.jsonand forced on withUseStaticWebAssets(). Without it (comment,:41-47) the pages render unstyled and never become interactive, so axe's contrast checks would be meaningless and the page would never signal Blazor readiness. - Rendering services (
:53-56):AddRazorComponents().AddInteractiveServerComponents(), thenAddMudServices(). - Extension-point stubs, before
AddUIShared(:62-66): scopedIAuthUIService,ITokenStorageService, andITokenRefresher, thenAddHttpContextAccessor()and the scopedAuthenticationStateProvider. The ordering comment (:58-61) states the mechanism,AddUIShared'sTryAdd*registrations defer to whatever is already present. - Real authentication and authorization (
:72-76):AddAuthentication(GalleryFakeAuthenticationHandler.SchemeName)plusAddScheme<AuthenticationSchemeOptions, GalleryFakeAuthenticationHandler>(...), thenAddAuthorization(), because the guarded pages'[Authorize]surfaces as endpoint metadata (comment,:68-71). - Canned notification services (
:81-83): scopedNotificationState,INotificationInboxUIService, andIPushNotificationUIService, so the notification pages discovered from theMMCA.Common.UIassembly render populated markup (comment,:78-80). - Notification scope (
:88): scopedINotificationScopeProviderbound to the sharedNullNotificationScopeProvider, whoseGetCurrentScopeKeyAsyncreturns null (MMCA.Common.UI/Services/Notifications/NullNotificationScopeProvider.cs:11-12). The comment (:85-87) explains the choice: the send page injects the scope provider to caption its auto-applied target, and the gallery is unscoped, so the null provider keeps the page rendering with the caption absent, which is the framework-default look the scan should measure. - Canned role administration (
:92): scopedIRoleAdminUIServicebound to StubRoleAdminUIService, so the shared/admin/rolesand/admin/roles/{role}pages render populated for the scan (comment,:90-91, citing ADR-116). - Module contribution (
:97): the singletonIUIModule, so the shared Router discovers the gallery's own/componentsand/gridpages (comment,:94-96). - Shared UI (
:102):AddUIShared(builder.Configuration)registers theApiSettings/LayoutSettingsbinding, the"APIClient"HttpClient, and the remaining shared services; the in-memoryApi:ApiEndpointfromappsettings.jsonsatisfies validation and deliberately points at the unroutablehttp://api.gallery.invalid(MMCA.Common.UI.Gallery/appsettings.json:2-4), and the client is never invoked becauseIAuthUIServiceis stubbed (comment,:99-101). - Build (
:104):builder.Build(), after which only middleware and endpoints are configured. - Request localization (
:111-115): buildsgalleryCulturesas[.. SupportedCultures.All, SupportedCultures.PseudoLocale]and applies it as the supported and supported-UI culture set overSupportedCultures.Default. The comment (:106-110) is explicit that this mirrors the real hosts' ADR-027 allowlist but additionally enablesqps-Plocunconditionally, because this host is unpackaged test infrastructure that is never deployed and the pseudo pass here is a required CI gate (PseudoLocalizationE2ETests, the rubric §27 resource-round-trip and text-expansion evidence). Production keepsqps-PlocDevelopment-only viaUseCommonRequestLocalization, and the comment closes with "Do not copy this into a real host".[Rubric §27, i18n]assesses whether localization is enforced rather than aspirational; this host is where the pseudo-locale evidence is produced. - Middleware (
:119-124):UseAuthentication()thenUseAuthorization()(WebApplication insertsUseRoutingahead of them automatically, comment:117-118), thenUseAntiforgery(), required because Razor Component endpoints carry anti-forgery metadata even though the gallery's interactive forms never POST over HTTP (comment,:122-123). - Endpoints (
:128-140):MapStaticAssetsis given the gallery's own{galleryAssemblyName}.staticwebassets.endpoints.jsonfor the same in-process self-host reason as above (comment,:126-127); a/healthendpoint returnsResults.Ok("Healthy")(:131);MapCommonServiceWorker()(:134) maps the installable-web-app worker that every consumer host maps and that the gallery'sApp.razorregisters throughpwa-register.js(comment,:133), so the gallery serves the same PWA surface the real hosts do (ADR-136); andMapRazorComponents<App>().AddInteractiveServerRenderMode().AddAdditionalAssemblies(typeof(MMCA.Common.UI._Imports).Assembly)(:138-140) makes the real shared pages routable alongside the gallery's own (comment,:136-137). - Return (
:142): the built-but-not-startedWebApplication, leaving the start mode to the caller.
- Assembly name and base dir (
- Why it's built this way: keeping the whole build in
BuildApprather thanProgram.cslets the E2E fixture host the identical configured app in-process on a real bound port viaStartAsync, notWebApplicationFactory's in-memory TestServer, which Playwright cannot reach over the wire. GalleryHostFixture therefore clears the URLs, bindshttp://127.0.0.1:0, and reads the ephemeral address back fromIServerAddressesFeature(MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26-37). This is deliberate CI infrastructure and is never shipped: the csproj setsIsPackable=falseand records that the project is deliberately outsideMMCA.Common.slnx(MMCA.Common.UI.Gallery/MMCA.Common.UI.Gallery.csproj:3-6), so it builds only by csproj path. - Where it's used: consumed by
MMCA.Common.UI.Gallery/Program.cs:7(thedotnet runentry) and by every E2E test through GalleryHostFixture (MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryHostFixture.cs:26), against which the axe and render suite runs: LoginPageE2ETests, RegisterPageE2ETests, ForgotPasswordPageE2ETests, ResetPasswordPageE2ETests, SessionsPageE2ETests, ComponentsPageE2ETests, GridPageE2ETests, NotificationPagesE2ETests, DarkModeE2ETests, MobileTopRowE2ETests, StickySidebarE2ETests, WebVitalsE2ETests, ServiceWorkerE2ETests (MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:17), and PseudoLocalizationE2ETests. NotificationPagesHiddenRoutingTests also callsBuildAppdirectly (MMCA.Common.UI.E2E.Tests/NotificationPagesHiddenRoutingTests.cs:109). - Caveats / not-in-source: the class summary (
MMCA.Common.UI.Gallery/GalleryHost.cs:18-23) lags the body in two ways. It names only/login,/register, and/componentswhileBuildAppalso serves the guarded notification and sessions pages and the/gridvirtualization page, and it still describes the stub set as including an "anonymous auth state", whereas GalleryAuthenticationStateProvider mirrors the request rather than forcing anonymous.
Per-project test rollup
The remaining test types in G25 are logged here by project rather than as individual ### sections (per the TESTS note): each project below is a Microsoft Testing Platform / xUnit v3 assembly, and the table gives its purpose and testing style (unit, integration, architecture-fitness, E2E, or component). The table covers 61 assemblies and 2995 types.
| Test project (assembly) | Types | Purpose and style |
|---|---|---|
MMCA.ADC.Conference.API.Tests |
28 | Unit tests for the Conference module's API layer: one test class per controller (events, sessions, speakers, partners, sponsors, rooms, activities, categories, questions, session assets), plus policy and convention gates for permission grants (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Authorization/ConferencePermissionGrantsTests.cs:21), export authorization (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Conventions/EntityExportAuthorizationTests.cs:23), conditional writes (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Conventions/ConditionalWriteConventionTests.cs:22), and localized error resources (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Localization/ConferenceErrorResourcesTests.cs:15). |
MMCA.ADC.Conference.Application.Tests |
200 | Unit tests for Conference command/query handlers, validators, and DTO mappers: the module's largest suite, covering CQRS handler behavior in isolation. |
MMCA.ADC.Conference.Domain.Tests |
36 | Unit tests for Conference domain entities and invariants: factory methods, Result<T> validation rules, and aggregate behavior. |
MMCA.ADC.Conference.Infrastructure.Tests |
12 | Unit tests for Conference infrastructure: EF entity configuration against a test DbContext (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Persistence/ConferenceEntityConfigurationTests.cs:18), the module database seeder, the Sessionize import service, output-cache eviction of session scores, and the AI session-scoring path: the scoring service, its response guardrail, and the module's AI guardrail registration, each driven through a private stub IChatClient (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoringServiceTests.cs:22, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoreResponseGuardrailTests.cs:18, MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/ConferenceAiGuardrailsRegistrationTests.cs:17). |
MMCA.ADC.Conference.IntegrationTests |
47 | Integration tests exercising the Conference module against a real SQL Server instance, covering repositories, handlers, and the outbox end to end. |
MMCA.ADC.Conference.Scoring.Evaluation.Tests |
10 | Evaluation suite for the AI session scorer: a golden corpus (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenCase.cs:106) replayed through GoldenReplayTestsBase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenReplayTests.cs:35), the scoring prompt contract pinned through PromptContractPinTestsBase (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/PromptContractTests.cs:180), and a live-judge test class (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/LiveJudgeTests.cs:39). |
MMCA.ADC.Conference.Shared.Tests |
9 | Unit tests for the Conference Shared project: the DTO contracts (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/EventDTOTests.cs:6, plus the session, speaker and category DTO tests), current-event defaults and selection (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/CurrentEventSelectorTests.cs:11), the disabled event-live validation service (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/DisabledEventLiveValidationServiceTests.cs:12), and the Sessionize code format (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/SessionizeCodeFormatTests.cs:9). |
MMCA.ADC.Conference.UI.Tests |
129 | bUnit component tests for Conference Blazor pages and components: rendering, parameter binding, and interaction behavior. |
MMCA.ADC.CrossService.IntegrationTests |
13 | Cross-service integration tests: CrossServiceFixture hosts Identity, Conference and Engagement in one process against a real Testcontainers SQL Server and a Testcontainers RabbitMQ, so the genuine MassTransit outbox-to-broker-to-consumer round trip runs, and runs Notification as two replicas over a Testcontainers Redis SignalR backplane (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/Infrastructure/CrossServiceFixture.cs:15-:16, :28, :87). |
MMCA.ADC.E2E.Tests |
97 | Playwright end-to-end tests driving the deployed/running ADC UI through full user journeys (registration, check-in, bookmarks, admin flows). |
MMCA.ADC.Engagement.API.Tests |
12 | Unit tests for the Engagement module's API layer: one test class per controller (bookmarks, check-ins, points, live polls, live-poll voting, session questions), plus permission grants (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Authorization/EngagementPermissionGrantsTests.cs:22), the owner-or-admin filter vocabulary (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Authorization/OwnerOrAdminFilterVocabularyTests.cs:24), conditional-write conventions (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Controllers/ConditionalWriteConventionTests.cs:13), and localized error resources (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Localization/EngagementErrorResourcesTests.cs:15). |
MMCA.ADC.Engagement.Application.Tests |
73 | Unit tests for Engagement command/query handlers, validators, and DTO mappers (points, badges, check-ins). |
MMCA.ADC.Engagement.Domain.Tests |
12 | Unit tests for Engagement domain entities and invariants. |
MMCA.ADC.Engagement.Infrastructure.Tests |
8 | Unit tests for Engagement EF Core configuration and persistence-layer helpers. |
MMCA.ADC.Engagement.IntegrationTests |
22 | Integration tests exercising the Engagement module against a real SQL Server instance. |
MMCA.ADC.Engagement.Shared.Tests |
6 | Unit tests for Engagement cross-cutting Shared-project helpers. |
MMCA.ADC.Engagement.UI.Tests |
54 | bUnit component tests for Engagement Blazor pages and components (check-in scanning, badges, points displays). |
MMCA.ADC.Gateway.Tests |
11 | In-process host tests for the ADC YARP Gateway: route mapping over a WebApplicationFactory (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/RouteMapTests.cs:38), hardening, security headers and robots.txt over ProductionHostApplicationFactory<TEntryPoint>, graceful shutdown through GracefulShutdownTestsBase<TEntryPoint>, and AppHost-to-Bicep parity (AppHostBicepParityTests, MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/AppHostBicepParityTests.cs:24). |
MMCA.ADC.Identity.API.Tests |
11 | Unit tests for the Identity module's API controllers (auth, registration, MFA). |
MMCA.ADC.Identity.Application.Tests |
36 | Unit tests for Identity command/query handlers, validators, and authentication/authorization services. |
MMCA.ADC.Identity.Domain.Tests |
6 | Unit tests for Identity domain entities and invariants. |
MMCA.ADC.Identity.Infrastructure.Tests |
12 | Unit tests for Identity EF Core configuration and persistence-layer helpers, plus the avatar normalizer SkiaSharpImageProcessor, which must turn an untrusted upload into an exact-size square JPEG with all metadata stripped and fail an unsupported format or an oversized declared frame as a validation error rather than an exception (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Services/Users/SkiaSharpImageProcessorTests.cs:14). |
MMCA.ADC.Identity.IntegrationTests |
40 | Integration tests exercising the Identity module (auth flows, roles, MFA) against a real SQL Server instance. |
MMCA.ADC.Identity.Shared.Tests |
2 | Unit tests for Identity Shared-project contracts: the role-to-permission grant map and the disabled attendee query service. |
MMCA.ADC.Identity.UI.Tests |
18 | bUnit component tests for Identity Blazor pages and components (login, registration, account management). |
MMCA.ADC.Notification.API.Tests |
1 | Unit tests for the Notification module's API surface. |
MMCA.ADC.Notification.Application.Tests |
4 | Unit tests for Notification command/query handlers and recipient-resolution logic. |
MMCA.ADC.Notification.IntegrationTests |
10 | Integration tests exercising the Notification module's outbox and delivery paths. |
MMCA.ADC.ServiceBusEmulator.IntegrationTests |
3 | Integration tests verifying the local Azure Service Bus emulator round-trips messages used by the outbox/event-bus pipeline. |
MMCA.ADC.Services.Tests |
23 | Unit tests for the service-host gRPC servers and their client adapters invoked directly (no booted host): wire round trips, RpcException-to-Result mapping, the live-channel ingress bounds, the export timestamp contract (including the Conference feedback-answer half of the user-data export), and the Conference and Identity warm-up paths. |
MMCA.ADC.UI.Web.Tests |
9 | In-process host tests for the ADC Blazor Web host, each class sharing one ConferenceUiHostApplicationFactory built on ProductionHostApplicationFactory<TEntryPoint> with Program as the entry point (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/ConferenceUiHostApplicationFactory.cs:17): security headers (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/SecurityHeadersTests.cs:17), UI rate limiting (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/UiRateLimitingTests.cs:18), fallback authorization (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/FallbackAuthorizationTests.cs:18), the client-config endpoint (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/ClientConfigEndpointTests.cs:14), Serilog bootstrap (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/SerilogBootstrapTests.cs:16), and the bounded circuit handler (MMCA.ADC/Tests/Hosts/MMCA.ADC.UI.Web.Tests/BoundedCircuitHandlerTests.cs:16). |
MMCA.Common.AI.Tests |
39 | Unit tests for the framework's AI package: the chat-client decorators (bounded, guardrail, prompt tagging, usage recording), provider selection and the adapter factory (MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Providers/AdapterFactoryTests.cs:16), DI registration and settings binding, the guardrail set (content policy, request redaction, streamed guardrails, tool policy, and their registration, for example MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Guardrails/ContentPolicyGuardrailTests.cs:12), and the evaluation helpers (recorded responses, the replay chat client, and reference golden-replay and prompt-contract pins built on the shared bases, MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Evaluation/ReferenceGoldenReplayTests.cs:17, MMCA.Common/Tests/Core/MMCA.Common.AI.Tests/Evaluation/ReferencePromptContractTests.cs:11). Stub chat client, guardrail and tool-policy doubles live under Fixtures/. |
MMCA.Common.API.Tests |
215 | Unit tests for the framework's API base classes and conventions: controller bases, versioning, rate limiting, and parameter descriptors. |
MMCA.Common.Application.Tests |
428 | Unit tests for the framework's CQRS/Application layer: command/query handler bases, the decorator pipeline (caching, authorization, multi-tenancy), and validation conventions. The framework's largest single suite. |
MMCA.Common.Aspire.Hosting.Tests |
6 | Unit tests for the framework's Aspire AppHost extensions used by consumer AppHost projects. |
MMCA.Common.Aspire.Tests |
54 | Unit tests for the framework's Aspire integration helpers: readiness budgets, environment gates, and probe paths. |
MMCA.Common.Aws.LocalStack.Tests |
15 | Emulator tier for the AWS adapter: one pinned LocalStack container per assembly (S3, SQS, SNS, Secrets Manager, Systems Manager and DynamoDB) with a fixed dummy key pair that never touches the AWS default credential chain (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.LocalStack.Tests/LocalStackFixture.cs:19), driving the SQS transport round trip (.../AmazonSqsTransportLocalStackTests.cs:40), S3 file storage (.../S3FileStorageLocalStackTests.cs:15), SNS push (.../SnsPushLocalStackTests.cs:26), the data-protection key ring (.../AwsDataProtectionLocalStackTests.cs:16) and Secrets Manager configuration (.../AwsSecretsManagerConfigurationLocalStackTests.cs:17). Integration style; needs Docker. |
MMCA.Common.Aws.Tests |
19 | Unit tests for the AWS adapter package: the SQS transport (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Messaging/AmazonSqsTransportTests.cs:21), S3 storage, object URLs and the client factory (.../Storage/S3FileStorageServiceTests.cs:20), SNS native push, device registration and its DI call (.../Notifications/SnsNativePushSenderTests.cs:21, .../Notifications/AddAmazonSnsPushTests.cs:19), RDS IAM authentication (.../Rds/AwsRdsIamAuthenticationTests.cs:18), the Secrets Manager configuration provider and its extensions (.../Secrets/AwsSecretsManagerConfigurationProviderTests.cs:19), data protection and service registration, and the vendor's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:13). Unit style. |
MMCA.Common.Azure.Tests |
17 | Unit tests for the Azure adapter package: the Azure Service Bus transport and the emulator branch's detection (MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Messaging/AzureServiceBusTransportTests.cs:20, .../Messaging/ServiceBusEmulatorSupportTests.cs:15), AzureNotificationHubDeviceRegistrar and the native-push user tags (.../Notifications/AzureNotificationHubDeviceRegistrarTests.cs:17, .../Notifications/NativePushUserTagsTests.cs:11, ADR-044), Blob file storage (.../Storage/AzureBlobFileStorageServiceTests.cs:17), Key Vault configuration and blob-backed data protection (.../AzureKeyVaultConfigurationExtensionsTests.cs:27, .../AzureDataProtectionExtensionsTests.cs:19), Entra SQL authentication (.../SqlClient/SqlClientEntraAuthenticationTests.cs:14), the Azure Monitor exporter and the Live Metrics off switch (.../Telemetry/AzureMonitorExporterTests.cs:16, .../Telemetry/LiveMetricsConfigurationTests.cs:21), service registration, and the vendor's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:13). Unit style. |
MMCA.Common.Benchmarks |
6 | BenchmarkDotNet performance benchmarks for hot paths in the framework (not a correctness suite). |
MMCA.Common.Domain.Tests |
67 | Unit tests for the framework's base Domain types: entities, aggregate roots, specifications, and domain events. |
MMCA.Common.Gateway.Tests |
10 | Unit tests for the framework's reusable YARP Gateway extensions. |
MMCA.Common.Gcp.Emulator.Tests |
7 | Emulator tier for the Google Cloud adapter: a pinned fake-gcs-server container, run as a generic container because it has no Testcontainers module (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FakeGcsServerFixture.cs:18), and a Firestore emulator fixture (.../FirestoreEmulatorFixture.cs:14), driving Cloud Storage file storage (.../GcsFileStorageEmulatorTests.cs:14), the GCS-backed data-protection key ring (.../GcsDataProtectionEmulatorTests.cs:24) and FCM push over Firestore through a recording messaging client (.../FcmPushFirestoreEmulatorTests.cs:22). Integration style; needs Docker. |
MMCA.Common.Gcp.Tests |
34 | Unit tests for the Google Cloud adapter package: Cloud Storage file storage, object URLs and the client factory (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsFileStorageServiceTests.cs:21), the KMS-encrypted GCS data-protection key ring (.../DataProtection/GcpKmsXmlEncryptionTests.cs:15, .../DataProtection/GcsXmlRepositoryTests.cs:14), FCM native push and device registration (.../Notifications/FcmNativePushSenderTests.cs:21), Secret Manager configuration (.../Secrets/GoogleSecretManagerConfigurationProviderTests.cs:19), Cloud SQL IAM authentication (.../CloudSql/CloudSqlIamAuthenticationTests.cs:16), the Google Cloud telemetry exporter and its auth handler against an in-process OTLP receiver (.../Telemetry/GoogleCloudTelemetryExportTests.cs:22, .../Telemetry/OtlpTestReceiver.cs:19), and the negative half of the cloud-vendor rules: an assembly declared cloud-neutral that names a Google SDK type, references a Google SDK assembly or references the adapter fails them (.../CloudVendorRulesTests.cs:20). In-memory fakes stand in for each Google client. Unit style. |
MMCA.Common.Grpc.Tests |
28 | Unit tests for the framework's gRPC infrastructure helpers, including the service-token authentication used between internal callers. |
MMCA.Common.Infrastructure.PostgreSQL.Tests |
19 | Tests for the framework's PostgreSQL engine: the ApplicationDbContext implementation and the built-in PostgreSQL broker transport (MessageBus:Provider=PostgreSql) run against a real server, where the transport schema is created before the bus needs it, the ambient MMCA-* headers survive the SQL transport's message rows, a failed consume comes back through the transport's native delayed delivery, and a registered data-source configurator (the IAM token path) authenticates the broker's own connections (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.PostgreSQL.Tests/PostgreSqlBrokerTransportTests.cs:59, with its fixture at :34). Integration style over Testcontainers. |
MMCA.Common.Infrastructure.Redis.Tests |
4 | Integration tests that run the framework's Redis-backed caches and the distributed rate limiter's Lua counter against a real Redis Testcontainer. |
MMCA.Common.Infrastructure.SQLServer.Tests |
8 | Integration tests that run the shipped SQL Server context and DbContextFactory against a real SQL Server in Docker (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.SQLServer.Tests/SQLServerPersistenceTests.cs:44). |
MMCA.Common.Infrastructure.Tests |
583 | Unit tests for the framework's core Infrastructure layer: SQL Server ApplicationDbContext, interceptors, repositories, outbox processing, and EF conventions. The framework's largest suite overall. |
MMCA.Common.LoadTests |
14 | Weekly load tests of the framework's real persistence stack over a temp-file SQLite database: the dynamic paged-query path at 100,000 rows and outbox throughput at 10,000 events (MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/PagedQueryLoadTests.cs:13). |
MMCA.Common.Shared.Tests |
64 | Unit tests for the framework's Shared-layer cross-cutting helpers (identifier types, common value objects). |
MMCA.Common.Testing.Aspire |
10 | Not a test assembly but the separate MMCA.Common.Testing.Aspire package (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Aspire/MMCA.Common.Testing.Aspire.csproj:3): a collection fixture that boots a real Aspire AppHost and waits for per-resource health, plus typed assertions for the framework wiring contracts (health, liveness, JWKS, h2c, data sources) (:4), consumed by other projects' AppHost smoke tests. |
MMCA.Common.Testing.Aspire.AppHostTests |
3 | Architecture-fitness/smoke tests validating the framework's own sample AppHost boots correctly under the Testing.Aspire harness. |
MMCA.Common.Testing.Aspire.Tests |
8 | Unit tests for the MMCA.Common.Testing.Aspire package's own fixture types. |
MMCA.Common.Testing.Azure |
1 | Not a test assembly but the separate MMCA.Common.Testing.Azure package: its one type is ServiceBusEmulatorFixtureBase, the bounded, phase-named Service Bus emulator broker-parity fixture a consumer subclasses (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Azure/Fixtures/ServiceBusEmulatorFixtureBase.cs:54). |
MMCA.Common.Testing.Azure.Tests |
4 | Unit tests for the MMCA.Common.Testing.Azure package: the container-free contract of ServiceBusEmulatorFixtureBase (the image pin, the bounded-phase budgets, the admin-plane connection-string composition, the no-bus default, and the process-global MassTransit entity defaults the emulator's TTL quota forces, MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Azure.Tests/Fixtures/ServiceBusEmulatorFixtureBaseTests.cs:15), plus the package's half of the cloud-vendor rules (.../CloudVendorRulesTests.cs:15). Unit style. |
MMCA.Common.Testing.E2E.Tests |
2 | Unit tests for the E2E test configuration: the millisecond settings parse with the invariant culture and refuse zero, negative and non-finite timeouts by falling back to the default (a 0 means no timeout to Playwright), and because each fact sets and restores a process-wide environment variable the class runs alone in its own collection (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.E2E.Tests/Infrastructure/E2ETestConfigurationTests.cs:21, :9). Unit style. |
MMCA.Common.Testing.Tests |
36 | Unit tests for the framework's general-purpose MMCA.Common.Testing package (test doubles, builders, assertion helpers) shipped for consumer use. |
MMCA.Common.UI.E2E.Tests |
33 | Playwright end-to-end tests for the framework's shared UI Gallery/demo host, including the installable-web-app service worker in a real browser (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:15). |
MMCA.Common.UI.Tests |
269 | bUnit component tests for the framework's reusable Blazor UI package (shared components, pages, services). |
MMCA.Common.UI.Web.Tests |
38 | Unit tests for the framework's Blazor Web host services (used by consumer Web hosts): server-side token storage, web form-factor detection, UI rate limiting and the bounded circuit handler (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Hardening/BoundedCircuitHandlerTests.cs:20), the trusted-caller handler, header registration and service discovery (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Security/TrustedCallerHandlerTests.cs:13), the Blazor CSP policy provider (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Security/BlazorCspPolicyProviderTests.cs:26), and the same-origin API proxy end to end (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/SameOriginProxy/SameOriginApiProxyTokenTests.cs:15). |
Test suites by module
MMCA.Common, the framework suite (Tests/ mirrors Source/)
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.Common.Shared.Tests |
64 | The innermost layer: the Result/Error/ErrorType pattern and its extensions, value objects (Money, Email, Address, DateRange, ...) and their factory-method invariants, DTO/paging contracts, and the striped keyed lock behind KeyedSemaphoreStripe (mutual exclusion per key, independent progress across stripes, release on the exception path, and a bounded table size no matter how many caller-supplied keys arrive, MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Concurrency/KeyedSemaphoreStripeTests.cs:12). Two files defend contracts that only bite in a head with no ASP.NET pipeline: MoneySerializationTests pins the round trip of Money's private [JsonConstructor], which is also the constructor EF Core uses to materialize the owned type, so a materializer yielding a null currency must fail fast rather than surface a half-built value object (.../ValueObjects/MoneySerializationTests.cs:11); and SupportedCulturesTests pins SupportedCultures.ResolveClosest, the fallback chain a head applies when it resolves its own culture instead of getting request localization's Accept-Language matching for free, since an Android device reports a specific culture (es-MX) while the allowlist holds a neutral one and without the language-level match a Spanish phone would silently start in English (.../Globalization/SupportedCulturesTests.cs:11, ADR-027 / §27). The smart enumeration Enumeration<TEnumeration> has reflection-based member discovery, Result-returning FromValue/FromName lookups (case-insensitive by name, an UnknownValue/UnknownName error rather than an exception) and type-guarded equality pinned, so two enumerations that happen to share an integer value are never equal and never collide in a hash set (.../ValueObjects/EnumerationTests.cs:10). KeysetPaginationTests covers the keyset ("seek") paging value types, the request's clamp semantics, the page result, and the cursor codec's round-trip, version gate and rejection of anything malformed, which matters because a cursor is a client-held token and a corrupted one must be refused rather than quietly paged from the top (.../Abstractions/KeysetPaginationTests.cs:10, ADR-055); RoleValueTests pins that RoleValue.Validate compares roles case-insensitively no matter which comparer the caller's set was built with, after a set built with the default ordinal comparer rejected a correctly spelled role that differed only in casing (.../Auth/RoleValueTests.cs:13). Its newest four all exist because a contract is shared by callers that must never disagree: ErrorTypeSeverityTests pins the severity ranking every transport edge uses to pick the status for a combined result, including that the 400-family ranks equal and that ties keep the earliest error (.../Abstractions/ErrorTypeSeverityTests.cs:10); ProblemDetailsResultReader parses an RFC 9457 payload back into Error instances and lives here rather than in the API package precisely so the UI can reach it (.../Http/ProblemDetailsResultReaderTests.cs:15); NotificationScopeKey ships its formatter and the regex that guards it together, and every key the formatter produces is asserted against that pattern, which is the default the notification hub enforces (.../Notifications/NotificationScopeKeyTests.cs:14); and ModuleNameConventionsTests pins the MMCA.{App}.{Module}.{Layer} parse shared by persistence (SQL schema and data-source names) and the logging decorators' scope enrichment, including the namespaces that carry no module at all (.../Conventions/ModuleNameConventionsTests.cs:15). ConcurrencyETag, the weak entity tag that carries the concurrency token, is pinned here too, one layer below the filter that consumes it (.../Http/ConcurrencyETagTests.cs:10, ADR-035). PasswordComplexityTests pins the shared strong-password rule that both the server validator and the client form attribute evaluate, with Unicode-aware character classes: an accented or CJK letter is a letter (never the special character), a non-ASCII uppercase letter is uppercase, and a non-ASCII decimal digit is a digit (MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Auth/PasswordComplexityTests.cs:12). RetryAfterHeaderTests pins both halves of the 429 Retry-After contract: the server formats a lease's hint in whole seconds rounded up, and the client reads either header form back and refuses to wait out one longer than HttpResilienceDefaults.MaxRetryAfterWait (MMCA.Common/Tests/Core/MMCA.Common.Shared.Tests/Resilience/RetryAfterHeaderTests.cs:13, RetryAfterHeader); and UsPostalRegionsTests covers UsPostalRegions, the USPS regions by code, code/name lookup and the ZIP / ZIP+4 format (.../ValueObjects/Contact/UsPostalRegionsTests.cs:10). Pure unit tests, no DI or DB. |
MMCA.Common.Domain.Tests |
67 | The entity hierarchy (BaseEntity→AuditableBaseEntity→AuditableAggregateRootEntity), domain-event collection, SetItems<T>/GetChildOrNotFound<T>, specifications, and the PiiAttribute/anonymization boundary plus the logging/telemetry redaction half of the [Pii] contract (masks marked members so a data subject's values never reach logs, MMCA.Common/Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs:12, ADR-005 / §30, PiiRedactor). OwnedByUserSpecification<TEntity, TIdentifierType>, the reusable "rows this caller created" criteria, is covered with a fake that overrides the virtual CreatedBy getter, because that audit field is stamped by the infrastructure layer through EF's change tracker and is otherwise unsettable from a test (.../Specifications/OwnedByUserSpecificationTests.cs:8). SpecificationCompositionTests pins HOW the boolean composers build their criteria rather than only what the composed predicate answers, and neither property is visible from IsSatisfiedBy: the composed tree must contain no InvocationExpression, because a provider that cannot unwrap one (Cosmos) throws at translation time on an ANDed specification, and it must be built once per instance, because the query pipeline reads Criteria on every request while the old implementation rebuilt the tree every time (.../Specifications/SpecificationCompositionTests.cs:18, ADR-018). QuerySpecificationTests covers the builder state a QuerySpecification carries beyond its predicate (includes, ordering, paging, tracking, soft-delete scope) and pins the base chain the SpecificationsDoNotNavigateToOtherEntities fitness rule keys on (.../Specifications/QuerySpecificationTests.cs:14). OutputCacheEvictionRequested is asserted as a shape rather than a behavior (members, schema version, empty-tags default) because it is the framework's own published integration event and every host that consumes it must be able to deserialize it forever (.../IntegrationEvents/OutputCacheEvictionRequestedTests.cs:12, ADR-010). Its newest file is the RefreshSession record (BR-205/206): the token is stored only as an upper-case hex SHA-256 digest and never in the clear, captured client metadata is truncated to its column width rather than overflowing at insert, revocation records when, why and the successor, and re-revoking keeps the first reason (.../Auth/RefreshSessionTests.cs:13, ADR-097). Its two newest types are fixtures inside PiiRedactorTests, PiiBase and PiiOverride (MMCA.Common/Tests/Core/MMCA.Common.Domain.Tests/Privacy/PiiRedactorTests.cs:32, :38). Pure unit tests over the framework domain primitives. |
MMCA.Common.Application.Tests |
428 | The CQRS engine: the decorator pipeline in its registered nesting order, which now carries two validating rings (commands: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→Transactional→handler, MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:134-140; queries: FeatureGate→Authorization→Logging→Caching→Validating→Timeout→handler, .../DependencyInjection.cs:138-143, both registered innermost-first because Scrutor's TryDecorate applies in reverse, .../DependencyInjection.cs:115), the opt-in MiniProfiler decorators added by AddApplicationProfiling (.../DependencyInjection.cs:565) and the CqrsMetrics counters/histograms (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Decorators/CqrsMetricsTests.cs:20), ModuleLoader topological ordering, DomainEventDispatcher plus the swallow-and-log SafeDomainEventHandler base (.../DomainEvents/SafeDomainEventHandlerTests.cs:14), validation, the IMessageBus abstraction, entity-query projection/paging and the per-type filter strategies, the cross-source CrossSourceSpecification helper, the magic-byte upload sniffer behind ImageContentSniffer (.../ImageContentSnifferTests.cs:12, ADR-045), and the notification read/send handlers driven by an injected TimeProvider test clock. Two older files defend non-obvious properties rather than behavior: PagingMathTests pins the page arithmetic (.../Services/Query/PagingMathTests.cs:12), and QueryFilterServicePropertyCacheTests asserts on the real static property cache inside QueryFilterService to prove that caching a miss never happens, since filter names arrive in the query string and a negatively-cached miss would let any caller grow a process-lifetime dictionary one bogus name at a time while the request still returned a tidy 400 (.../Services/Filtering/QueryFilterServicePropertyCacheTests.cs:14, §11/§12). The hoisted user use cases hold a large block: ChangePasswordHandlerBaseTests pins verify-before-write ordering and the no-save-on-invariant-failure rule (.../Users/ChangePasswordHandlerBaseTests.cs:15, ADR-032); DeleteUserHandlerBaseTests pins the owner-or-privileged-role gate, the delete-then-anonymize-then-save ordering and the post-commit queue (.../Users/DeleteUserHandlerBaseTests.cs:14); UserOwnershipRule gets its own tests for the self-service authorization check that was written out four times across the two apps before the hoist (.../Users/UserOwnershipRuleTests.cs:11); SoftDeletedUserValidatorTests proves the BR-133 check is one query with the soft-delete global filter deliberately bypassed (.../Users/SoftDeletedUserValidatorTests.cs:13); and ExportUserDataHandlerBaseTests drives ExportUserDataHandlerBase<TUser, TQuery> through the ownership gate, the read-only account load and the best-effort section fan-out, where a section that throws or reports itself unavailable must degrade and let the rest of the package travel (a data-subject request is a legal deadline, so a partial export beats a failed one) while a cancelled section propagates instead (.../Users/ExportUserDataHandlerBaseTests.cs:17, ADR-076 / §30). The password-recovery pair is the newest addition to that family and both files are written around anti-enumeration: a malformed address, an unknown address, a throttled request and a failed send are all indistinguishable successes, and the mail that IS sent carries both the prefilled link and the raw token (.../Users/ForgotPasswordHandlerBaseTests.cs:20), while every rejection on the reset leg collapses to one generic Auth.InvalidResetToken, the write happens only when the aggregate accepts it, the token is consumed before the save, and a successful reset clears the lockout so the new credential is immediately usable (.../Users/ResetPasswordHandlerBaseTests.cs:18, ADR-091). RefreshSessionManagementTests covers the per-device surface layered on that workflow: the sid claim stamped on every issued access token, a refresh stamping the successor session's id rather than the predecessor's, an older access token with no sid still accepted, and a device list that carries the client fields and never the token material (.../Auth/RefreshSessionManagementTests.cs:26, ADR-097). Alongside them, SoftDeletedUserCache has its key shape and culture invariance pinned because the API middleware reads that exact key (.../Auth/SoftDeletedUserCacheTests.cs:13); CachingDecoratorConstructorSelectionTests pins that the container picks the logger-bearing constructor on both caching decorators, since if selection ever flipped the decorators would keep working and every test would stay green while production silently stopped reporting cache failures (.../Decorators/CachingDecoratorConstructorSelectionTests.cs:21); and CachingDecoratorTenantScopingTests covers the tenancy half of the same pair, where ICacheService is a singleton that cannot see the scoped tenant, so the key transformation is the isolation (.../Decorators/CachingDecoratorTenantScopingTests.cs:16, ADR-073 / §11). The two newer decorator rings have their own files: AuthorizationCommandDecoratorTests (.../Decorators/AuthorizationCommandDecoratorTests.cs:11) and its query twin cover the permission check that runs inside the pipeline rather than only at the controller, and TimeoutCommandDecoratorTests (.../Decorators/TimeoutCommandDecoratorTests.cs:9) covers the per-handler execution budget. Read-pipeline determinism and projection pushdown form their own block (ADR-055, §12): EntityQueryPipelineOrderingTests pins that a paginated read always ends up with a total order, because Skip/Take over a partial order is undefined and lets one row appear on two consecutive pages while another appears on none, while an unpaginated read is deliberately left alone (.../Services/EntityQueryPipelineOrderingTests.cs:16), with the tie-break parameter covered directly and its omission pinned as behavior-preserving (.../Services/QueryFieldServiceTieBreakTests.cs:11); EntityQueryService gains a projection path that selects DTO columns and never calls the mapper, with a fallback to materialize-then-map when the read cannot be projected (.../Services/EntityQueryServiceProjectionTests.cs:19), and its two-constructor arrangement is proved to actually resolve under Microsoft.Extensions.DependencyInjection, which has no notion of an optional dependency (.../Services/EntityQueryServiceResolutionTests.cs:19); PushNotificationDTOProjectorTests is the safety property underneath all of that, since the two paths are chosen by whether a projector is registered and a divergence would make the response depend on a DI detail (.../Notifications/PushNotificationDTOProjectorTests.cs:16). BestEffortTests covers the helper the fire-and-forget call sites share: the success path is transparent, a failure is swallowed with exactly one Warning and one metric increment, and cancellation is deliberately NOT part of the swallow (.../Services/BestEffortTests.cs:13). The newest block is composition and contract guards. EventUpcasterRegistry covers the schema-evolution ladder: identity for an unregistered contract, a V1→V2→V3 chain applied in order even when registered out of order, envelope (MessageId/DateOccurred) preservation across every hop whether or not the author copies it, and the three constructor-time misconfigurations (duplicate source, cycle, self-map) that must throw naming the offender (.../Services/EventUpcasterRegistryTests.cs:19, ADR-090); its sample contracts live in the test assembly only so the framework's frozen contract snapshot never churns on them. ApplicationPipelineCompositionTests covers AddMmcaApplicationPipeline, the one call that replaces the manual AddApplication → scan → AddApplicationDecorators sequence, and the registration-time seal that turns a handler registered after the decorators (which Scrutor would silently leave undecorated) into a startup failure (.../ApplicationPipelineCompositionTests.cs:20, ADR-014). PackageGraphPurityTests reads the csproj itself, because the IL-based fitness rules can only see what the code uses and are blind to what a PackageReference drags in: one web or persistence package would enter the graph of every consumer of this host-agnostic package and nothing else in the build would notice (.../PackageGraphPurityTests.cs:15, §32). CqrsContractInspectorTests covers the inspector behind the fitness rule that a command marker is never handled as a query and that a request's declared result type matches its handler's (.../UseCases/CqrsContractInspectorTests.cs:7), and ScopedIntegrationEventHandlerBaseTests pins the base that opens one DI scope per delivery (integration-event handlers are singletons and cannot hold a scoped service), disposes it on every path, logs a failure exactly once before letting it propagate so the delivery mechanism can redeliver, and passes cancellation through unlogged because host shutdown is not a delivery failure (.../DomainEvents/ScopedIntegrationEventHandlerBaseTests.cs:15). Its newest types are fixtures: Harness and RecordingSetter back MarkAllNotificationsReadHandlerTests (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Notifications/MarkAllNotificationsReadHandlerTests.cs:141, :207), and ScanFailingAssembly backs ModuleLoaderTests (.../Modules/ModuleLoaderTests.cs:325). Three newer files: AuthenticationServicePasswordRehashTests pins the sign-in rehash, where a password that verifies against a hash the hasher reports as outdated is re-derived and stored, driven by a hasher mock that answers true for the 64-byte legacy shape and false for the 69-byte current one, the same split the shipped hasher makes (MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Auth/AuthenticationServicePasswordRehashTests.cs:29, ADR-102); ChangePreferencesCommandValidatorBaseTests pins the shared preference-change validator, where a missing payload is one clear failure, a null field means leave unchanged, and culture and theme are checked against the same definitions the domain uses (.../Users/UseCases/ChangePreferences/ChangePreferencesCommandValidatorBaseTests.cs:18), beside its handler tests (.../Users/UseCases/ChangePreferences/ChangePreferencesHandlerBaseTests.cs:16); and UsAddressValidationRulesTests covers the US address rules, a USPS region code and a 5-digit or ZIP+4 ZIP code, with an empty field reporting only that it is required (.../Validation/UsAddressValidationRulesTests.cs:12). The framework's largest suite by breadth; fast unit tests with mocked infrastructure. |
MMCA.Common.Infrastructure.Tests |
583 | The widest layer: EF repositories + Unit of Work, the multi-database resolver/registry (DataSourceResolver, EntityDataSourceRegistry, DbContextFactory) with its transaction coverage (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/DbContexts/DbContextFactoryTransactionTests.cs:37) and the cross-data-source degrade convention (.../Persistence/DataSources/CrossDataSourceDegradeConventionTests.cs:24), the outbox processor plus its wake signal (.../Persistence/Outbox/Processing/OutboxSignalTests.cs:13) and the consumer-side EfInboxStore idempotency ledger (.../Persistence/Inbox/EfInboxStoreTests.cs:27, ADR-021), caching, JWT issuance + JWKS + the login-attempt lockout service (.../Auth/LoginProtectionServiceTests.cs:14), column-level encryption (.../Persistence/Encryption/EncryptedStringConverterTests.cs:6), the filtered-unique-index soft-delete convention (.../Persistence/Conventions/SoftDeleteUniqueIndexConventionTests.cs:24), the SignalR push + live-channel plumbing, the message-bus implementations, the polyglot Cosmos-config portability suite (ADR-018), and the in-repo disaster-recovery database-restore drill (.../Resilience/DatabaseRestoreDrillTests.cs:18, a CI-gated RTO baseline, ADR-009 / §29). Three files are pure §12 performance guards and are the only place the emitted SQL or the tracker's work is inspected at all: QueryParameterizationTests asserts that the dynamic-LINQ filter and sort strategies send their values as SQL parameters rather than inlined literals, which is what decides whether SQL Server reuses a plan and whether EF's compiled-query cache hits (.../Persistence/Specifications/QueryParameterizationTests.cs:26); SaveChangeDetectionTests pins that a save runs change detection exactly once and that suppressing the extra passes lost the tracker no actual changes (.../Persistence/Interceptors/SaveChangeDetectionTests.cs:24); and PeriodicBackgroundServiceTests drives PeriodicBackgroundService deterministically through a FakeTimeProvider clock to cover the enablement gate, the startup delay, interval-driven cycles, and the failing-cycle-never-kills-the-loop contract (.../Scheduling/PeriodicBackgroundServiceTests.cs:15). A second cluster hardens the save path: DbContextFactorySaveIntegrityTests pins the post-loop assertion that turns silent data loss into a failure, since the save loop is bounded and in-process domain-event dispatch runs inside it, so a handler can leave tracked changes behind that the loop will never reach (.../Persistence/DbContexts/DbContextFactorySaveIntegrityTests.cs:29); DbContextFactoryCommitAmbiguityTests covers the case where a commit-phase failure has an unknowable outcome, because the database may have applied the transaction and lost only the acknowledgement (.../Persistence/DbContexts/DbContextFactoryCommitAmbiguityTests.cs:34); DomainEventCaptureExclusionTests proves event capture is scoped-exclusion aware (.../Persistence/Interceptors/DomainEventCaptureExclusionTests.cs:26); and EFRepositoryAuditStampTests pins that the repository's own save entry points stamp the acting user like the unit of work does, after they were found calling the plain EF overloads and attributing everything written through them to the system sentinel (.../Persistence/Repositories/EFRepositoryAuditStampTests.cs:32). One pair sits outside persistence, the IDistributedLock pair, where RedisDistributedLock must acquire through a single atomic SET NX PX carrying a TTL and release through a compare-and-delete on the owner token (.../Concurrency/RedisDistributedLockTests.cs:15), with InProcessDistributedLock held to the same contract as the no-Redis fallback (.../Concurrency/InProcessDistributedLockTests.cs:12). The enterprise wave contributes four clusters here. Tenancy: ApplicationDbContextTenantFilterTests proves over real SQLite that the named Tenant global filter composes with SoftDelete rather than replacing it, that a null-tenant system context still sees every row, and that one cached model serves two tenants at once with disjoint results (.../Persistence/Tenancy/ApplicationDbContextTenantFilterTests.cs:15); TenantSaveChangesInterceptorTests covers the write side, where an insert is stamped from the scope and any modify/delete that would cross the boundary throws (.../Persistence/Tenancy/TenantSaveChangesInterceptorTests.cs:12); TenantDataSourceTargetTests covers the (source, tenant) expansion the background sweeps run on, because a tenant with its own database has its own outbox, inbox and trail tables and nothing else opens that database (.../Persistence/Tenancy/TenantDataSourceTargetTests.cs:18); AddMultiTenancyTests pins the DI surface, the fail-closed defaults and the startup validation (.../Persistence/Tenancy/AddMultiTenancyTests.cs:19); and TenantContextTests pins the one-write scope (.../Context/TenantContextTests.cs:10). Audit trail: AuditTrailSaveChangesInterceptorTests asserts every case against a real SQLite round-trip, so what is asserted is what commits, one summary row per insert/delete, one row per changed property on a modify, nothing for an entity marked modified but unchanged, and a [Pii] property recording the redaction token and never the clear value (.../Persistence/AuditTrail/AuditTrailSaveChangesInterceptorTests.cs:18, ADR-075 / §30); AuditTrailReader covers newest-first paging with clamped arguments and an empty answer when the source has no trail table (.../Persistence/AuditTrail/AuditTrailReaderTests.cs:20); AuditTrailCleanupJobTests covers the retention purge and its two do-nothing paths (.../Persistence/AuditTrail/AuditTrailCleanupJobTests.cs:22). Scheduler: ScheduledJobRunner is driven over a FakeTimeProvider and an in-memory ScheduledJobs table so the schedule arithmetic is exact rather than timing-dependent, covering registration sync, the configuration override, the claim lease, outcome recording, and the missed-run policy where a clock that jumped past many occurrences runs once and advances rather than storming (.../Scheduling/ScheduledJobRunnerTests.cs:22, ADR-074); CronosNextOccurrenceTests pins the cron grammar, the strictly-after semantics and the UTC-only clock that keeps a schedule stable across both daylight-saving transitions (.../Scheduling/CronosNextOccurrenceTests.cs:11); AddScheduledJobsTests pins the opt-in DI shape (.../Scheduling/AddScheduledJobsTests.cs:16); SchedulerMetrics pins the meter name a host registers for export and the instrument names, units and tags an operator builds dashboards on (.../Scheduling/SchedulerMetricsTests.cs:13); and SchedulerModelGateTests builds the real model under each combination of Scheduler:Enabled and data source to prove a host that never opted in gets exactly the model it had before the scheduler shipped (.../Scheduling/SchedulerModelGateTests.cs:26). HybridCache: HybridCacheService runs against a real in-process AddHybridCache with a recording L2, and the key-shape assertions carry the whole design, since an entry written by the old cache at the same logical key must be a clean miss, never the WRONGTYPE fault that motivated the split (.../Caching/HybridCacheServiceTests.cs:24, ADR-077). The read contract (ADR-055) is covered against a real provider rather than a mock, because all of it is translation: EFReadRepositoryKeysetPagingTests covers keyset paging end to end, cursor round-trips, next-page detection, the non-unique sort key that makes the identifier tie-break load-bearing, and the two rejection paths of an unknown sort column and a malformed cursor (.../Persistence/Repositories/Read/EFReadRepositoryKeysetPagingTests.cs:16); EFReadRepositorySpecificationTests covers the specification-driven members plus tracking and soft-delete scope (.../Persistence/Repositories/Read/EFReadRepositorySpecificationTests.cs:16); EFReadRepositoryProjectedFilterTests pins the ignoreQueryFilters flag on GetProjectedAsync against the production named soft-delete filter, because without it any caller needing deleted rows (an admin restore screen, a GDPR export) had to abandon the projection (.../Persistence/Repositories/Read/EFReadRepositoryProjectedFilterTests.cs:16); SpecificationEvaluator is exercised over SQLite for the ordering chain bound back to its concrete key type by reflection, includes with the collection split-query switch, and Skip/Take (.../Persistence/Specifications/SpecificationEvaluatorTests.cs:15); and PushNotificationProjectionTranslationTests proves a real provider translates the projection into SQL, enum-to-string conversion included, a failure that would otherwise surface only at runtime (.../Persistence/PushNotificationProjectionTranslationTests.cs:15). Its newest work is the auth, outbox and messaging edges. Auth: PasswordResetTokenServiceTests verifies the token lifecycle single use, the wrong-token attempt cap, the per-email request throttle, the address normalization that keeps casing variants on one record, and the hash-at-rest guarantee (.../Auth/PasswordResetTokenServiceTests.cs:18, ADR-091); EFRefreshSessionStoreRotationTests asserts the rotation claim against a real SQLite database because the guarantee is the database's, not the change tracker's, since two requests presenting the same still-live token each load their own tracked copy with RevokedAt null and an in-memory check-then-act would let both mint a successor (.../Persistence/Auth/EFRefreshSessionStoreRotationTests.cs:27, ADR-050); RefreshSessionCleanupServiceTests covers the retention sweep, where a row is a candidate once it stopped being usable rather than once it was created (.../Persistence/Auth/RefreshSessionCleanupServiceTests.cs:33); and PasswordHasherSecurityTests pins the cryptographic parameters rather than the round trip, since a hasher that hashes and verifies with the same weak settings still round-trips: two known-answer tests recompute the digest independently, a negative one proves the iteration count participates in verification, and reflection pins the private constants so lowering one fails the build (.../Auth/PasswordHasherSecurityTests.cs:18, ADR-102). Outbox and migrations: OutboxAdministration is the operator path that gives a dead-lettered event a way back into delivery instead of leaving "wait for the retention sweep" as the only ending, exercised over a real SQLite outbox table so the set-based replay update is the one that would run in production (.../Persistence/Outbox/Administration/OutboxAdministrationTests.cs:27); OutboxProcessorOrderingTests covers ordered delivery, where rows sharing an OrderingKey must reach the bus one at a time in OccurredOn order and that guarantee must survive the two things a batch-local sort cannot cover, a successor arriving in a later batch and a second replica polling the same table (.../Persistence/Outbox/Processing/OutboxProcessorOrderingTests.cs:35); DependencyInjectionOutboxGateTests pins the registration gate, where an in-process host pays for no outbox table or poll loop, a broker host always gets one, and asking for a broker without the outbox fails at registration rather than silently dropping every cross-service event (.../DependencyInjectionOutboxGateTests.cs:15, ADR-003); MigrationApplyProofTests runs a committed migration from a separate fixture assembly against a real SQLite file through DbContextFactory, the same call the "Migrate" strategy makes, so "the framework applies migrations" is proved rather than assumed (.../Persistence/MigrationApplyProofTests.cs:35), with DbContextFactoryMigrationTargetTests proving which sources the factory acts on (.../Persistence/DbContexts/DbContextFactoryMigrationTargetTests.cs:25). Messaging: UpcastingIntegrationEventConsumer is the draining consumer bound to a retired contract, deduping on the original message id, upcasting to the terminal contract, dispatching to that contract's handlers, and recording the inbox row only after every handler succeeded (.../Messaging/Consumers/UpcastingIntegrationEventConsumerTests.cs:26, ADR-090); EventUpcasterStartupValidatorTests covers the startup check over the registered ladders (.../Messaging/Consumers/EventUpcasterStartupValidatorTests.cs:20); IntegrationEventConsumerHarnessTests drives the consumers through a real MassTransit bus on the in-memory transport, because the registration binding, the MessageId surviving serialization, and a handler exception genuinely becoming the Fault<TEvent> that FaultIntegrationEventConsumer subscribes to are broker behaviors no direct Consume call can show (.../Messaging/Consumers/IntegrationEventConsumerHarnessTests.cs:28, and the fault consumer's own file at .../Messaging/Consumers/FaultIntegrationEventConsumerTests.cs:15, ADR-087); and InboxDisabledWarningService covers the one startup line that keeps a disabled dedup store from looking exactly like an enabled one (.../Persistence/Inbox/InboxDisabledWarningServiceTests.cs:11). Two more round it out: SqlServerUniqueConstraintViolationDetectorTests covers both halves of the detector, the authoritative provider error numbers and the message fallback, building a real SqlException through the provider's own non-public factory and reporting itself skipped rather than failing the build if a provider upgrade moves that factory (.../Persistence/SqlServerUniqueConstraintViolationDetectorTests.cs:17); and ConnectionStringSettingsValidatorTests covers the one startup rule that spans two configuration sections, a host must be able to reach some database declared either top level or as a named DataSources entry, which is why it is a validator rather than a [Required] annotation (.../Settings/ConnectionStringSettingsValidatorTests.cs:15, ADR-070). The permission-grant snapshot matches role names case-insensitively on read and keeps an entry across the gap before the next rebuild while still expiring it when refreshes keep failing (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Auth/PermissionGrantCacheTests.cs:18), beside its EF store (.../Persistence/Auth/EFPermissionGrantStoreTests.cs:29). Three files pin engine differences: the keyset seek predicate has to follow each engine's null placement (SQL Server and SQLite sort nulls first ascending, PostgreSQL the reverse), evaluated by SQLite because the defect is about three-valued SQL logic (.../Persistence/Repositories/KeysetQueryBuilderNullOrderingTests.cs:22); the notification configurations must emit PostgreSQL index filters when a PostgreSQL-only host substitutes its engine (.../Persistence/Configuration/NotificationConfigurationEngineTests.cs:23); and CosmosDbContextTests covers the Cosmos context (.../Persistence/DbContexts/CosmosDbContextTests.cs:12). Its newest files are privacy, startup and transport guards. AuthCacheKeyPrivacyTests pins that a user's email address never appears in a cache key the login-protection, password-reset or email-confirmation services read, write or remove, so it can never reach a Redis key name, a log line or an exception message, while two spellings of one address still land on one key (.../Auth/AuthCacheKeyPrivacyTests.cs:27); OutputCacheNamespaceTests pins the per-application namespace on the Redis output-cache store, so two applications sharing one Redis never serve or evict each other's entries (.../Caching/OutputCacheNamespaceTests.cs:16); CloudAdapterTripwireTests covers the startup tripwire that fails a host whose enabled NativePush section still resolves to the inert core sender, naming the adapter call (.../Configuration/CloudAdapterTripwireTests.cs:19); and StartupConfigurationSummaryService is held to one Information line per host start naming the version, engines, bus, cache and tenancy the host resolved, never a secret, plus a Warning for a RabbitMQ host with delayed redelivery off (.../Configuration/StartupConfigurationSummaryServiceTests.cs:20). The broker-transport extension point has three files: BrokerTransportRegistrationTests covers how AddBrokerMessaging resolves the transport at registration time (.../Messaging/Transports/BrokerTransportRegistrationTests.cs:17), BrokerTransportPoliciesTests pins the public receive policies every transport applies, so an adapter transport applies exactly what the built-in one does (.../Messaging/Transports/BrokerTransportPoliciesTests.cs:15), and PostgreSqlBrokerTransportTests covers the built-in PostgreSQL transport's selection, policies, shared data source and schema migration without opening a connection, leaving the wire round trip to MMCA.Common.Infrastructure.PostgreSQL.Tests (.../Messaging/Transports/PostgreSqlBrokerTransportTests.cs:24). NpgsqlDataSourceConfiguratorTests pins the optional INpgsqlDataSourceConfigurator extension point, where every context for one connection string shares a single configured NpgsqlDataSource (one pool, one password provider) built once and disposed with the container (.../Persistence/DataSources/NpgsqlDataSourceConfiguratorTests.cs:26). Mostly unit with EF-InMemory/SQLite boundaries (no real SQL Server here: that tier is MMCA.Common.Infrastructure.SQLServer.Tests). |
MMCA.Common.Infrastructure.Redis.Tests |
4 | The one tier in the framework that runs the shipped caches against a real Redis. The unit tier mocks IDistributedCache, which means it asserts the calls the cache makes and never the storage format Redis ends up holding, and that is a blind spot with teeth: Redis keys are typed, INCR creates a string, and the IDistributedCache Redis provider stores every entry as a hash of absexp/sldexp/data, so mixing the two at one key round-trips flawlessly against a mock and answers WRONGTYPE in production, on the ADR-029 rate-limit and lockout counters. DistributedCacheServiceRedisTests starts a redis:7-alpine Testcontainer, builds DistributedCacheService exactly as AddCaching does when both a distributed cache and a multiplexer are registered, and proves the increment-then-read round-trip, that increments carry a TTL so a counter can never lock a subject out forever, that concurrent writers may undercount but must never leave the key unreadable (the honest statement of the current read-modify-write contract), prefix invalidation over a real SCAN, and a plain set/get/remove smoke (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/DistributedCacheServiceRedisTests.cs:27). HybridCacheServiceRedisTests is its ADR-077 sibling and exists for the same reason at a higher stake: only a real server can distinguish "the two substrates share a keyspace" from "they do not", so it proves an entry written by either cache is a soft miss (never a fault) for the other, that prefix eviction runs both patterns and also drops the evicting process's own local copy, and that increments stay monotonic across two instances sharing one Redis (.../HybridCacheServiceRedisTests.cs:35). RedisFixedWindowRateLimiterRedisTests covers the distributed rate limiter the same way: the unit tier only asserts the Lua script text and what the limiter sends to a mocked database, so whether the script increments, sets a TTL on the creating increment, and returns a count the limiter can compare is proven here against the server (.../RedisFixedWindowRateLimiterRedisTests.cs:16). Needs Docker, so the project is outside the slnx and runs in the redis-integration CI job (MMCA.Common/.github/workflows/ci.yml:741). Integration style. |
MMCA.Common.Infrastructure.SQLServer.Tests |
8 | The real-server tier for the shipped SQLServerDbContext and DbContextFactory. The unit tier mocks or substitutes the provider, so it cannot see three behaviours only a server has: SET IDENTITY_INSERT is session state that holds only on the connection that ran it, a rowversion is generated by the server on every write, and the outbox row has to reach the database in the same SaveChanges as the aggregate that raised the event (MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.SQLServer.Tests/SQLServerPersistenceTests.cs:44). The other seven types are in-file fixtures (SqlThing and its configuration, RecordingDomainEventDispatcher, FixedCurrentUserService, FixedAssemblyProvider, NoTenantContext, .../SQLServerPersistenceTests.cs:223). It needs a Docker daemon, so it sits outside MMCA.Common.slnx and runs in its own sqlserver-integration CI job (MMCA.Common/.github/workflows/ci.yml:806). Integration style. |
MMCA.Common.API.Tests |
215 | The presentation pipeline: ApiControllerBase.HandleFailure ErrorType-to-HTTP mapping, the exception-handler chain, the [Idempotent] filter + Idempotency-Key replay, permission policies/ownership filters, correlation, the JWKS and OIDC-discovery endpoints, the session-cookie auth handler/refresher/jar, the shared notification + device controllers, the public-endpoint output-cache policy, the database-initialization startup (the SQLite-EnsureCreated-under-Migrate path), and the error-message localization edge (localizes the human-readable message while leaving the machine Code/ProblemDetails title untouched, ADR-027 / §27). UserAccountAuthControllerBaseTests covers the shared account-management controller base that landed with the hoisted user use cases, driving change-password, preferences and delete through mocked handlers plus ICurrentUserService (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Controllers/Auth/UserAccountAuthControllerBaseTests.cs:17), and PasswordResetAuthControllerBaseTests is its recovery sibling, pinning the two endpoints and their attributes, because both actions are anonymous by necessity so losing the per-IP policy or the idempotency marker would break nothing visible and simply leave an unauthenticated endpoint unthrottled (.../Controllers/PasswordResetAuthControllerBaseTests.cs:23, ADR-091). The rate-limiting files are worth reading together (ADR-019, §11): RateLimitPartitionTests drives the global limiter's exemption and partition-key logic directly (infrastructure paths and gRPC content types bypass, anonymous traffic gets the no-limiter partition, an authenticated caller partitions by name then user_id then remote IP) and then the per-IP anti-spray policy, which deliberately fails open, since an unattributable request gets no limiter rather than sharing one bucket with every other unattributable caller (.../Startup/RateLimitPartitionTests.cs:17); AuthControllerBaseRateLimitTests asserts the attachment by reflection, because the policy is applied with an attribute and a dropped attribute breaks nothing loudly (the endpoint simply stops being throttled), pinning LoginAsync/RegisterAsync carrying the auth-IP policy and RefreshAsync deliberately not carrying it, since refresh is automatic and every Blazor Server circuit shares the UI host's IP (.../Controllers/AuthControllerBaseRateLimitTests.cs:21); RateLimitingSettingsTests guards the defaults and their binding, load-bearing because the permit-count overload of AddCommonRateLimiting delegates to them (.../RateLimiting/RateLimitingSettingsTests.cs:13); RedisFixedWindowRateLimiter covers the permit comparison, the one-shot TTL on the key that opens a window, and the fail-open posture on a Redis fault, since a limiter that failed closed would turn a cache outage into a site-wide 429 storm (.../RateLimiting/RedisFixedWindowRateLimiterTests.cs:16); and RateLimitAlgorithmSelectionTests resolves each partition's factory and inspects the limiter it actually builds, because the partition key alone never says whether a request is counted in memory or against the shared Redis counter (.../Startup/RateLimitAlgorithmSelectionTests.cs:21). Enterprise-wave files: TenantResolutionMiddlewareTests covers TenantResolutionMiddleware at the edge, the configured claim-then-header strategy order, the trimmed value, the fail-closed RequireTenant rejection as ProblemDetails, the excluded paths, and the two shapes that must pass every request straight through (.../Middleware/TenantResolutionMiddlewareTests.cs:17); the CSV export pair holds CsvWriter to RFC 4180 field by field (quote only what needs it, double an embedded quote, CRLF line endings, exactly one BOM, .../Export/CsvWriterTests.cs:8) while EntityControllerBaseExportTests drives the page-loop that exists because the query pipeline has no IAsyncEnumerable path, asserting the fan-in across pages, the short-first-page early stop, and the truncation marker appended exactly at the row cap including both page-boundary edges (.../Controllers/EntityControllerBaseExportTests.cs:25, ADR-078); DataExportControllerBaseTests pins the shipped DSAR endpoint, the dated download a subject receives, the ProblemDetails failure path, and the two attributes that are its whole security posture, asserted directly because nothing else fails when one is dropped (.../Controllers/Privacy/DataExportControllerBaseTests.cs:29, ADR-076 / §30). Conditional writes are covered in layers: SupportsIfMatchAttributeTests pins where the filter takes the token from and which status a conflict gets (.../Concurrency/SupportsIfMatchAttributeTests.cs:17) and EntityControllerBaseETagTests pins that a single read emits the token as a weak ETag so a client can turn straight around and send it as If-Match, while a DTO with no token gets no header (.../Controllers/EntityControllerBaseETagTests.cs:22, ADR-035). Next to them IdempotencyFilterPassthroughTests guards a premise rather than a behavior: a request without an Idempotency-Key header passes through untouched, which is what makes attaching [Idempotent] to an existing POST a non-breaking change for every client already calling it (.../Idempotency/IdempotencyFilterPassthroughTests.cs:22). OutputCacheEvictionHandler closes the cross-service cache loop: every tag on the message is evicted, a single tag's failure is swallowed and logged rather than rethrown (rethrowing would redeliver the message, re-evict the tags that already succeeded, and eventually dead-letter a message whose only consequence is a cache entry that expires on its own), and cancellation still propagates (.../Caching/OutputCacheEvictionHandlerTests.cs:20, ADR-026). Its newest block is contract and startup guards. OpenApiBaselineTests fetches the framework-owned OpenAPI document from a real in-memory host, normalizes it and diffs it against a committed openapi-baseline.v1.json, so an SDK or Asp.Versioning.OpenApi bump, a change to the framework's OpenAPI registration, or a change to the generated ProblemDetails schema fails here instead of reaching consumers unnoticed (.../OpenApi/OpenApiBaselineTests.cs:35, §9); ProblemDetailsRoundTripTests closes the loop between the two halves of the error contract by emitting through the real controller path with the serializer options MVC uses on the wire and reading the payload back with ProblemDetailsResultReader, which is what keeps the reader honest (.../Controllers/ProblemDetailsRoundTripTests.cs:22); MiddlewarePipelineBuilder covers the named-step edge pipeline, its default order, the insert/replace/remove operations a host customizes it with, and the invariants Build re-checks so a customized pipeline fails at startup rather than misrouting at runtime (.../Startup/MiddlewarePipelineBuilderTests.cs:12); ForwardedJwtBearerSecurityTests pins two executable security invariants, that the signature algorithm stays RS256 and that RequireHttpsMetadata resolves secure-by-default everywhere except Development, by executing the real registration and reading the resulting options back with nothing fetched from the authority (.../Startup/ForwardedJwtBearerSecurityTests.cs:22, §11), with the authority-resolution half in .../Startup/JwtAuthorityExtensionsTests.cs:13; EntityControllerBaseReadSpecificationTests pins the read-scoping hook, that every read action (both list overloads, lookup, by-id and export) asks GetReadSpecificationAsync for the rows this caller may see while a controller that overrides nothing queries exactly as unscoped as it always did (.../Controllers/EntityControllerBaseReadSpecificationTests.cs:29); and ModuleHostExtensionsTests covers the host-side module wiring (.../Startup/ModuleHostExtensionsTests.cs:22). CurrentUserTargetingContextAccessorTests rounds the row out on the feature-flag side, pinning which claim carries the user id (the Targeting filter hashes it, so a rollout is only sticky per user if that id is stable) and that an anonymous request produces an empty context rather than an error, because a feature filter must never be able to fail a request (.../FeatureManagement/CurrentUserTargetingContextAccessorTests.cs:15). The OpenAPI pair is pinned from the host side: AddCommonOpenApiHostRegistrationTests has this project play the host so the XML-comment source generator intercepts its own AddOpenApi() calls, and a host swapping its hand-written registration for the framework pair must get exactly the document plain AddOpenApi() produces (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/OpenApi/AddCommonOpenApiHostRegistrationTests.cs:36), while MapCommonOpenApi() declares its document anonymous under the fallback authorization policy and still maps nothing in Production (.../OpenApi/MapCommonOpenApiAuthorizationTests.cs:23). DesignTimeDatabaseInitializationTests pins the guard that skips schema initialization during build-time OpenAPI generation, honoured only in Development (.../Startup/DesignTimeDatabaseInitializationTests.cs:23). The claims-only session-cookie mode set by the same-origin proxy hands script a claims-only token instead of the credential (.../SessionCookies/ClaimsOnlySessionCookieEndpointsTests.cs:24), and that token keeps every claim and the expiry but carries no signature (.../SessionCookies/SessionClaimsTokenTests.cs:15). ErrorHttpMappingTests is an exhaustiveness pin so a new ErrorType cannot silently fall back to 400 (.../Middleware/ErrorHttpMappingTests.cs:12); a cancellation the client did not cause falls through to the generic handler instead of being reported as a 499 disconnect (.../Middleware/OperationCanceledExceptionHandlerTests.cs:14); the CSV export decides end-of-data from the data, because a full clamped page can be shorter than the requested page size (.../Export/EntityCsvExporterTests.cs:13); and the two fail-closed ownership gates answer 403 for an unresolvable owner claim and 404 for a non-owner (.../Authorization/OwnershipHelperGateTests.cs:18). ConcurrencyConflictMetricsTests pins the persistence.concurrency.conflict counter: one increment, tagged with the entity type, for every optimistic-concurrency conflict the API answers and nothing for any other database update failure, with real EF Core exceptions raised against an in-memory SQLite database and measurements filtered by the file's own probe entity types because the meter is a process-wide static (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Concurrency/ConcurrencyConflictMetricsTests.cs:27). Unit tests of middleware/filters/controllers in isolation. |
MMCA.Common.Grpc.Tests |
28 | The gRPC transport boundary: Result to RpcException round-tripping, the JWT-forwarding client interceptor, and the Polly resilience pipeline on typed clients (retry, circuit-breaker, and fault-injection). GrpcResultExceptionInterceptor is a good example of a test written around a fixed defect: the error-carrying case keeps the shared ToRpcException mapping, while the empty-errors case (what the message-only constructors produce) used to discard the exception message entirely and answer a placeholder "Unspecified failure", leaving the caller with a failure and no cause; it now keeps StatusCode.Internal and carries the real message, because synthesizing an Error.Failure instead would have downgraded a server-side fault to InvalidArgument and blamed the caller (MMCA.Common/Tests/Presentation/MMCA.Common.Grpc.Tests/GrpcResultExceptionInterceptorTests.cs:24). Its newest file covers the decode leg, the client-side turn of an RpcException back into a Result with its errors intact (.../ResultGrpcExtensionsDecoderTests.cs:14). GrpcWireFormatTests pins the wire conventions the hand-written adapters shared by copy: both the Z-marked and the marker-less timestamp forms must land as DateTimeKind.Utc on the same instant (MMCA.Common/Tests/Presentation/MMCA.Common.Grpc.Tests/GrpcWireFormatTests.cs:18). The Authentication/ folder covers the service token end to end: ServiceTokenAuthenticationTests pins the receiving side, a non-default ServiceToken JwtBearer scheme (HS256 only, svc claim required) plus an InternalCaller policy that accepts either the user token or a service token, with the load-bearing negative that a valid service token never satisfies a plain [Authorize] endpoint even when it shares the user scheme's key and issuer (.../Authentication/ServiceTokenAuthenticationTests.cs:33); sibling classes cover the token provider (.../Authentication/ServiceTokenProviderTests.cs:18), the settings (.../Authentication/ServiceTokenSettingsTests.cs:13), the default-scheme guard (.../Authentication/ServiceTokenDefaultSchemeGuardTests.cs:16), the client registration and HttpClient builder extensions (.../Authentication/ServiceTokenClientRegistrationTests.cs:17, .../Authentication/ServiceTokenHttpClientBuilderExtensionsTests.cs:22), and the client interceptor (.../ServiceTokenClientInterceptorTests.cs:24). Unit tests asserting ADR-007 / ADR-009 behavior. |
MMCA.Common.Aspire.Tests |
54 | The service-defaults package: OutboxPollFilterProcessor (drops recurring outbox-poll spans from telemetry export), the SecurityHeadersMiddleware, the head-based trace-sampling cost knob (a ratio in (0,1) opts in, anything else samples everything so a typo cannot silently drop all telemetry, MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Telemetry/TracesSampleRatioTests.cs:11, §31), the metrics-instrumentation toggle (.../Telemetry/MetricsInstrumentationToggleTests.cs:16) and the Serilog host wiring (.../Logging/SerilogHostExtensionsTests.cs:19). It guards the one deliberate asymmetry in AddInfrastructureHealthChecks: a missing SQL connection string throws at startup when the host requires it, while absent Redis/RabbitMQ skip silently, and the optional checks carry HealthCheckTags.Optional so they never gate /health/ready (.../Health/InfrastructureHealthChecksTests.cs:16, §29). That optionality now has two files of its own after a production incident where readiness went hard-dependent on Redis: RedisPingHealthCheckTests covers the ping probe itself (.../Health/RedisPingHealthCheckTests.cs:16) and RedisReadinessSafetyTests pins that no Redis check can ever appear in the readiness set, because DistributedCacheService already degrades around a cache blip and an untagged check would instead take every replica out of rotation at once (.../Health/RedisReadinessSafetyTests.cs:24). A large half of the suite is §29 warm-up material: WarmupReadinessGate must stay closed until warm-up finishes, then latch open idempotently and safely under concurrency (.../Warmup/WarmupReadinessGateTests.cs:10), and the health-check face of that gate reports Unhealthy while the replica is still warming and Healthy the moment the gate opens, which is what actually keeps a cold replica out of the /health/ready rotation (.../Warmup/WarmupReadinessHealthCheckTests.cs:11); the warm-up hosted service must run every IWarmupTask once and open the gate even when a task fails or hangs (bounded by a per-task timeout), so a transient dependency outage cannot wedge a replica permanently out of rotation (.../Warmup/WarmupHostedServiceTests.cs:13, ADR-025); and SelfHttpWarmupTaskBase covers what the per-service copies each had to get right alone: port resolution under dynamic ports, the Testing short-circuit, the h2c version pin, and the non-fatal wrapper (.../Warmup/SelfHttpWarmupTaskBaseTests.cs:28). KestrelEndpointExtensionsTests pins deployment-shaped configuration with a technique worth borrowing, asserting on the plan a builder produced rather than on a running dependency: it reads the listener plan directly rather than through a bound server, because that plan decides whether a deployed revision answers its platform health probes at all (.../Kestrel/KestrelEndpointExtensionsTests.cs:14). The Azure-specific data-protection, Key Vault and Live Metrics tests live in MMCA.Common.Azure.Tests. Its gateway block covers the edge kit this package ships, the extension points ADC's Gateway host consumes rather than hand-writing (ADR-088): GatewayCorrelationMiddleware must leave a correlation ID on the request and the response, preserve a caller-supplied one end to end, and need nothing from DI, which is the whole reason it exists separately from the context-bound CorrelationIdMiddleware in MMCA.Common.API (.../Gateway/GatewayCorrelationMiddlewareTests.cs:15); the downstream health checks are pinned by name, tag and failure status, including that a repeated registration does not produce the duplicate check name the health-check service rejects at startup (.../Gateway/GatewayDownstreamHealthChecksTests.cs:17); the edge limiter covers its settings defaults, the bypass matcher, the fail-open posture on an unattributable client, and that registering twice does not throw (.../Gateway/GatewayRateLimitingTests.cs:21); and the CORS extension covers the allowed-origin policy a browser-facing edge needs (.../Gateway/GatewayCorsExtensionsTests.cs:19). ServiceDefaultsRetryTests pins the retry predicate every factory HttpClient gets, one retry for the idempotent verbs and none for POST and PATCH (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Tests/Resilience/ServiceDefaultsRetryTests.cs:19). Its newest files are edge and probe guards: GatewayRetryAfterTests pins the Retry-After the edge limiter's 429 carries, written by an OnRejected that keeps any callback the host configured before it (.../Gateway/GatewayRetryAfterTests.cs:18); CachedHealthEndpointHeadersTests holds the cached probe endpoints to the three no-cache headers the stock middleware writes, asserted against fixed values and against the stock /alive endpoint from the same host (.../Health/CachedHealthEndpointHeadersTests.cs:13); PostgreSqlHealthCheckDataSourceTests pins that the PostgreSQL readiness check probes the container's configured data source when there is one, because a check built from the bare connection string would authenticate without the password provider and fail against a healthy database (.../Health/PostgreSqlHealthCheckDataSourceTests.cs:21); and HostIntegrationTripwireTests covers the startup tripwire that fails a host whose configuration asks for a vendor integration with no adapter call registered, naming the missing call (.../Integrations/HostIntegrationTripwireTests.cs:15). Unit suite over the Aspire service-defaults package. |
MMCA.Common.Aspire.Hosting.Tests |
6 | The AppHost-tier package, tested without ever starting a distributed application: every case builds a DistributedApplicationBuilder, applies the extension, and asserts on the resource model it produced. H2cHealthCheckExtensionsTests covers WithH2cHealthCheck, the probe an AppHost needs because an Http2-only cleartext service cannot answer an ordinary HTTP/1.1 health request: the probe sends HTTP/2 with prior knowledge at the configured path, the check is keyed by endpoint so one resource can gate two, registration is idempotent per builder but its duplicate guard is not shared across builders, an undeclared endpoint name registers lazily rather than throwing, and unreachable/non-success/timeout all report Unhealthy with the reason (MMCA.Common/Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/H2cHealthCheckExtensionsTests.cs:17). ServiceBusEmulatorBrokerTests covers the local broker swap: the emulator image is pinned to a 2.x tag, both planes (AMQP and management) are published, host ports are left to Aspire, the emulator waits for the SQL Server it needs, the connection string and admin endpoint are derived from the allocated endpoints, and WithBroker selects the Azure Service Bus transport while the RabbitMQ overload stays unchanged (.../ServiceBusEmulatorBrokerTests.cs:16). E2eLiftTests covers the rate-limit lift the E2E stack asks for, both triggers (environment variable and call-site flag), the write-nothing default, and that the emitted keys are derived from the section-name constants owned by the settings types rather than spelled by hand, which is why the two settings packages are referenced from the test project and deliberately not from the AppHost package itself (.../E2eLiftTests.cs:24, and the reasoning is written into .../MMCA.Common.Aspire.Hosting.Tests.csproj). Unit style, no Docker. |
MMCA.Common.Gateway.Tests |
10 | The reusable gateway package extracted so ADC's Gateway host stops hand-writing its edge. AddMmcaGatewayTests pins the registration surface (both config filters and the transform provider), settings binding from the MmcaGateway section, the documented defaults when no section exists, and, as a fitness-style assertion inside the unit suite, that the Gateway assembly does not depend on MMCA.Common.Aspire, keeping the two hosting packages independently consumable (MMCA.Common/Tests/Hosting/MMCA.Common.Gateway.Tests/AddMmcaGatewayTests.cs:16). The two config filters are where the package earns its keep, because they are what let a route table live in configuration (ADR-089): GatewayClusterProfileConfigFilterTests covers defaults applied to a cluster that declares nothing, per-cluster overrides merged property by property, a cluster's own config beating both, and an invalid HTTP version or version policy failing with the cluster named (.../Configuration/GatewayClusterProfileConfigFilterTests.cs:14); GatewayHealthCheckDefaultsConfigFilterTests covers the passive defaults filled into a cluster with no health-check block, active checks staying off unless the host turns them on, and every explicitly written block being left exactly as written (.../Configuration/GatewayHealthCheckDefaultsConfigFilterTests.cs:13). Both filters assert that routes are left untouched, since a filter that quietly rewrote a route would be invisible until traffic misrouted. GatewayRoutePolicyTests covers the named per-route limiters: partitioning on the caller address for a client-IP policy, one shared bucket for a global one, fail-open on an unresolvable IP, a 429 rejection, an out-of-range budget throwing at registration rather than at the first request, and a real window that exhausts and then rejects (.../RateLimiting/GatewayRoutePolicyTests.cs:17). GatewayTraceHeaderTransformProviderTests covers the route/cluster trace headers stamped on every proxied request, including that a client-supplied value is replaced rather than appended to (.../Transforms/GatewayTraceHeaderTransformProviderTests.cs:15), and ForwardedHeadersExtensionsTests covers the three forwarded headers, the cleared known-proxy allow lists a container platform needs, and that the options are fresh per call so one host's customization cannot leak into another (.../ForwardedHeadersExtensionsTests.cs:16). GatewayRetryAfterTests pins the Retry-After on a route-policy 429 and that the package's internal copy of the header logic (the package references no MMCA package) produces exactly what the Shared helper the other MMCA limiters use produces, so the two cannot drift, with HintlessLease as its in-file lease fixture (MMCA.Common/Tests/Hosting/MMCA.Common.Gateway.Tests/RateLimiting/GatewayRetryAfterTests.cs:18, :95). Unit style. |
MMCA.Common.Testing.Tests |
36 | The suite that tests the test framework itself, so a regression in the shared scaffolding fails here rather than silently weakening every consumer suite. HandlerTestBaseTests drives HandlerTestBase<THandler> exactly as a consumer handler test would, registering repositories and relying on the pre-wired unit of work (MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/Support/HandlerTestBaseTests.cs:13), and DecoratorPipelineOrderTests runs DecoratorPipelineOrderTestsBase<...> against MMCA.Common's own registration sequence to prove the resolved pipelines nest in the ADR-014 order (.../DecoratorPipelineOrderTests.cs:21). Four files cover the helpers the cross-service tiers lean on, each one because the behavior would otherwise only be observable in a Docker-bound nightly: CrossServiceFixtureBase's container-free logic (connection-string composition and the first-write-wins environment snapshot, .../CrossServiceFixtureBaseTests.cs:13); JwtTokenGenerator.ConfigureInProcessTokenValidation, where what matters is that JWKS/OIDC discovery is switched off and the static committed key takes over, because a test host that still tries to discover fails at the first authenticated request with a network error rather than an auth error (.../JwtTokenGeneratorTests.cs:19); TestPolling.PollUntilAsync, which must stop at the first satisfying probe and on timeout return the last probed value so the caller's own assertion produces the failure message instead of a bare timeout (.../TestPollingTests.cs:11); and DependencyInjectionAssert.ReturnsSameCollection, which has to fail for a registration that hands back a different collection, the failure mode that silently drops everything chained after it (.../DependencyInjectionAssertTests.cs:12). Its newest files are the gateway scaffolding hoisted out of ADC: RecordingHttpForwarder, the fake that never proxies and echoes the destination, timeout, HTTP version and trace headers back so a consumer can assert its route table behaviorally (.../RecordingHttpForwarderTests.cs:16); MmcaGatewayHardeningTestsBase<TEntryPoint>, whose eight edge gates now live here once instead of per repo (.../MmcaGatewayHardeningTestsBaseTests.cs:20); and MiddlewarePipelineOrderTestsBase, applied to the framework's own pipeline through a body-less subclass (.../MiddlewarePipelineOrderTests.cs:10). The broker scaffolding, ServiceBusEmulatorFixtureBase, ships in MMCA.Common.Testing.Azure and is tested in MMCA.Common.Testing.Azure.Tests. Two small helper surfaces round it out, the rate-limiter test extensions that let a suite drive a limiter without wall-clock waits (.../RateLimiterTestExtensionsTests.cs:17) and the feature-management extensions that flip a flag inside a test host (.../FeatureManagementTestExtensionsTests.cs:16). Unit style. |
MMCA.Common.UI.Tests |
269 | Shared Blazor components (delete-confirmation, empty-state, the mobile card/infinite-scroll lists, notification bell/inbox/list/send pages, primitives), the MudBlazor theme/provider harness, HTTP-resilience/service-exception helpers, list-page state/query-state services, the primitive markup snapshots, the auth-form view-model validation (§24), and the i18n globalization pair (the [!!...!!] bracket-sentinel pseudo-localizer and the ResxMudLocalizer MudBlazor-chrome boundary) plus the auth-aware nav menu and its mobile top-row. On i18n (ADR-027 / §27): CultureSwitcherTests guards that the switcher delegates to ICultureApplier instead of navigating to /culture/set itself, since that URL is a server endpoint and a hard-coded navigation left MAUI Blazor Hybrid heads (which host no ASP.NET pipeline) routing it through the Blazor router onto the not-found page (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Globalization/CultureSwitcherTests.cs:17); EndpointCultureApplier covers the web default, which must route through /culture/set with a force load because only that round trip writes the cookie SSR prerender and the WASM runtime both read (.../Globalization/EndpointCultureApplierTests.cs:15); and DocumentLanguageTests covers the component that makes a hybrid head's lang attribute follow a culture switch, asserted here precisely because no automated accessibility gate can catch a wrong lang (axe checks presence and syntax only, .../Components/DocumentLanguageTests.cs:13). On theming, ThemeToggleTests covers the app-bar Day/Dark switch including that disposal unsubscribes so the long-lived ThemeService never calls back into a dead component (.../Components/ThemeToggleTests.cs:16, ADR-028), and ApiUserPreferenceWriterTests pins the two guards that keep a signed-out or stale session from spending one 401 per theme/culture toggle, a write that is best effort and whose caller never learns it failed, making a doomed request pure cost (.../Services/Preferences/ApiUserPreferenceWriterTests.cs:16). On write safety, EntityServiceBaseIdempotencyRetryTests pins the Idempotency-Key EntityServiceBase<TEntityDTO, TIdentifierType> emits on creates and only on creates, the key staying identical across every retry of one logical operation, and the retry predicate's shape (5xx yes, 501 no, 429 yes) plus cancellation aborting the pipeline; the retried cases pay the real Polly backoff, so each is driven through the smallest attempt count that proves the behavior and asserts on captured attempts, never on wall-clock timing (.../Services/EntityServiceBaseIdempotencyRetryTests.cs:21). Its capability and JS boundary: LazyJsModule pins the single-flight import() the UI services delegate to, because an unguarded _module ??= await import(...) lets two concurrent callers each start an import so the browser holds two module instances and the later assignment leaks the earlier reference, while a failed import must not be cached and disposal must tolerate a disconnected circuit (.../Services/LazyJsModuleTests.cs:14); CapabilityFallbackTests sweeps every null/neutral capability default, each of which must degrade (report unsupported, no-op, or return empty) and never throw, because these are what shared components resolve on a head with no native or browser override (.../Services/Capabilities/CapabilityFallbackTests.cs:12, ADR-042); and QrCodeImageTests covers the managed QR render, a PNG data URI with required alt text, nothing at all for a blank payload, and a re-encode when the payload or error-correction level changes (.../Components/QrCodeImageTests.cs:11, ADR-071). Its newest work is the shared read/refresh plumbing and the account surface. LatestLoadGuard is the generation counter behind every "supersede an in-flight load" fix in both apps, pinned once here (.../Common/LatestLoadGuardTests.cs:11), and UiReadCache covers the opt-in client read cache pages share (.../Services/Caching/UiReadCacheTests.cs:15). ErrorSummaryTests covers the form-level error block that turns a failed Result into an accessible summary (.../Components/ErrorSummaryTests.cs:18, §24), ApiFileDownloadButtonTests the authenticated download path (.../Components/ApiFileDownloadButtonTests.cs:24), InfiniteScrollSentinelTests the intersection-observer sentinel behind the mobile lists (.../Components/InfiniteScrollSentinelTests.cs:14), HttpResultExecutorTests the single place a UI service turns an HTTP response into a Result (.../Services/HttpResultExecutorTests.cs:15), and AbsoluteUrlAttributeTests the validation attribute the share/QR surfaces depend on (.../Validation/AbsoluteUrlAttributeTests.cs:14). The multi-device session surface lands here as a page plus a parser: SessionsTests renders the sign-out-other-devices page in its loading, data, empty and error states (.../Pages/Auth/SessionsTests.cs:27) and UserAgentSummaryTests pins the user-agent-to-readable-device summary shown on each row, which is presentation over an attacker-controlled string and therefore must never throw on garbage (.../Services/Auth/UserAgentSummaryTests.cs:13, ADR-097). OfflineFirstPageSnapshotTests adds golden-markup coverage for the offline-first page shell (.../Pages/Common/OfflineFirstPageSnapshotTests.cs:14). The WebAssembly half of the same-origin API proxy sends no Authorization header and always X-CSRF: 1 once Api:SameOriginApiEndpoint is set (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/SameOriginProxyClientTests.cs:26); TokenRefreshPipelineTests drives the real APIClient pipeline so the refresh POST cannot re-enter its own in-flight hydrate (.../Services/Auth/TokenRefreshPipelineTests.cs:20); IdempotentReadRetryTests gives the anonymous lookup services the same transient-failure retry policy instance, GET only (.../Services/Api/IdempotentReadRetryTests.cs:16); PagedReadAllTests pins paging until the reported total (.../Services/Api/PagedReadAllTests.cs:14); NotificationPageGateTests hides the notification pages for an opted-in host that never registered the notification UI (.../Notifications/NotificationPageGateTests.cs:16); and two smaller pins cover the MAUI back-navigation module release (.../Services/Navigation/MauiBackNavigationBridgeTests.cs:12) and the cookie-sync write outcome (.../Services/Auth/JsFetchSessionCookieSyncTests.cs:13), with InMemoryHubServer as an in-process SignalR stand-in (.../Services/Notifications/InMemoryHubServer.cs:17). The newest block covers layout extension points and page lifetimes: MainLayoutFooterComponentTests pins the footer slot a module fills, rendered inside the one contentinfo footer rather than as an extra column in the desktop flex row (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Layout/MainLayoutFooterComponentTests.cs:25); PublicAppBarComponentTests covers the signed-out app-bar slot for a control such as site search or a cart, in the bar and in the mobile quick-actions row, while the signed-in slot keeps its gate (.../Layout/PublicAppBarComponentTests.cs:25); NotFoundContentTests pins that the first module supplying not-found content wins, a re-executed 403 still renders Access Denied, and with no module supplying one the page is unchanged (.../Pages/NotFoundContentTests.cs:20); AuthSubmitLifetimeTests pins that the sign-in and registration submits stop when the user navigates away or a render-mode transition tears the page down, instead of completing into a component that is gone (.../Pages/Auth/Lifetime/AuthSubmitLifetimeTests.cs:27); DetailLoadStateContentTests renders a record that does not exist and a record that could not be fetched differently, offering a retry only for the second (.../Components/PageState/DetailLoadStateContentTests.cs:13); StaleToastSweeperTests covers the sweep that clears toasts on screen longer than the grace period while keeping one raised in the same moment as the navigation (.../Components/Notifications/StaleToastSweeperTests.cs:15); and MmcaThemeProvidersColorModeTests pins the opt-in that stamps the color mode onto the document on the first interactive render and on every change, and never calls it when off (.../Theme/MmcaThemeProvidersColorModeTests.cs:16). Rendered with bUnit (component-render unit tests via BunitComponentTestBase). |
MMCA.Common.UI.Web.Tests |
38 | The Blazor Server web-host pieces: ServerTokenStorageService (during SSR prerender tokens come from the HttpOnly session cookies; on the interactive circuit the access token is held in memory, hydrated single-flight, and refreshed proactively near expiry, while the refresh token is never readable, MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Services/ServerTokenStorageServiceTests.cs), the server form-factor probe (.../Services/WebFormFactorTests.cs), and BlazorCspPolicyProvider, which pins the enforced production Content-Security-Policy verbatim (connect-src locked to the configured API/Gateway origin, no unsafe-eval, permissive Report-Only degradation on an unparseable endpoint, .../Security/BlazorCspPolicyProviderTests.cs, §26). Twelve of its types cover the same-origin API proxy, booted in-process against a fake gateway (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/SameOriginProxy/ProxyTestHarness.cs:148): the bearer is attached server-side from the HttpOnly cookie and a racing refresh runs once per session (.../SameOriginProxy/SameOriginApiProxyTokenTests.cs:15), every unsafe method needs X-CSRF: 1 (.../SameOriginApiProxyCsrfTests.cs:12), a foreign Origin or non-same-origin Sec-Fetch-Site is refused 403 (.../SameOriginApiProxyOriginTests.cs:14), an undecided refresh (5xx, 429) keeps the cookies and answers 503 with Retry-After (.../SameOriginApiProxyRefreshOutcomeTests.cs:14), sign-in, refresh and sign-out keep the token pair in the cookies (.../SameOriginApiProxyAuthFlowTests.cs:20), SignalR negotiate and WebSocket upgrades (HTTP/1.1 and RFC 8441 extended CONNECT) carry the cookie bearer (.../SameOriginApiProxyHubTests.cs:29), a host that does not opt in is unchanged (.../SameOriginApiProxyOptInTests.cs:21), and the Server circuit trades only protected handoffs (.../SessionHandoffServicesTests.cs:14). UiRateLimitingRetryAfterTests pins the Retry-After the Blazor host's limiter writes on a per-IP rejection through the OnRejected callback AddUiRateLimiting installs (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Web.Tests/Hardening/UiRateLimitingRetryAfterTests.cs:17), and ServiceWorkerEndpointTests covers MapCommonServiceWorker: a JavaScript content type that is never cached and carries the package version, the configuration kill switch that serves the inert worker instead, the anonymous, undocumented endpoint metadata the browser's update check depends on, and the navigations-only, never-cache-a-page rule pinned on the script text (.../Pwa/ServiceWorkerEndpointTests.cs:20). Unit and in-process integration tests. |
MMCA.Common.UI.E2E.Tests |
33 | Playwright axe-core (WCAG 2.1 AA) + render-smoke over the backend-less Gallery host, driven from GalleryAxeTestBase: real Login/Register pages, the primitives/components showcase, and the shared Notification pages against stubbed collaborators, plus the dark-mode toggle, a Web-Vitals budget probe, and two i18n/mobile-parity gates, a qps-Ploc pseudo-locale round-trip asserting the [!! sentinel and no horizontal overflow under roughly 40% text expansion (ADR-027 / §27) and the culture+theme controls pinned into the mobile top-row below 1024px (§22). StickySidebarE2ETests illustrates why a rendered-behavior assertion beats a CSS assertion: it keeps the desktop sidebar pinned while the page scrolls, a behavior that broke because position: sticky resolves against the nearest ancestor scroll container and two innocuous rules (html, body { overflow-y: auto } and .page { overflow-x: hidden }, where a non-visible overflow-x forces the computed overflow-y from visible to auto) turned content-sized ancestors that never scroll into dead scrollports; the test asserts the pinning, so it catches any future ancestor that reintroduces one (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Layout/StickySidebarE2ETests.cs:23). Its newest pages are the password-recovery pair (.../ForgotPasswordPageE2ETests.cs, .../ResetPasswordPageE2ETests.cs:9, ADR-091), the sessions page (.../SessionsPageE2ETests.cs:13), a grid page added to the Gallery so the shared data grid is scanned like every other primitive (.../GridPageE2ETests.cs:16), and AuthOutcomeRulesTests, which pins the outcome vocabulary the auth flows assert on so the browser tests and the components cannot drift apart (.../AuthOutcomeRulesTests.cs:12). ServiceWorkerE2ETests covers the installable-web-app worker in a real browser: pwa-register.js registers it, and a navigation that cannot reach the network renders the framework offline page instead of the browser's error page, with the Gallery mapping the worker exactly as a consumer host does (MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Pwa/ServiceWorkerE2ETests.cs:15). Deliberately outside MMCA.Common.slnx; runs in CI's ui-e2e job across chromium, firefox and webkit. E2E/accessibility style. [Rubric §21, Accessibility] (assesses automated a11y gating): this is where the framework proves zero axe violations before downstream apps consume the pages. |
MMCA.Common.Benchmarks |
6 | A BenchmarkDotNet performance-smoke executable covering the two DB-free hot paths. SpecificationBenchmarks measures the per-instance compiled-expression cache behind Specification<TEntity, TIdentifierType>.IsSatisfiedBy (a cached-compile baseline versus the recompile-each-call anti-pattern) and the And/Or composition cost (MMCA.Common/Tests/Performance/MMCA.Common.Benchmarks/SpecificationBenchmarks.cs:14); QueryPipelineBenchmarks adds the read side, which runs on every list request in every consumer and regresses silently because the dynamic-LINQ predicate is re-parsed per call and the shaper reflects over the DTO: a single CONTAINS filter, a three-strategy mixed filter, dynamic sorting, and full-field versus sparse-fields= shaping of a 100-row page (.../QueryPipelineBenchmarks.cs:17). Deliberately outside MMCA.Common.slnx (like the Gallery), but not on-demand-only: CI's performance-smoke job (MMCA.Common/.github/workflows/ci.yml:332) runs the suite and then build/perfgate compares the results against the committed Tests/Performance/perf-baseline.json, failing on any violation of its allocation ceilings or machine-independent ratio floors, and on a rule naming a benchmark that produced no measurement, so the gate cannot pass vacuously (.../ci.yml:371). Moving a number deliberately means updating the baseline in the same PR. [Rubric §12, Performance & Scalability] (assesses measured, not assumed, hot-path cost): this is the evidence harness, and the baseline file turns it from a runs-clean smoke into a regression gate. Performance-smoke style. |
MMCA.Common.LoadTests |
14 | Load tests over the framework's real persistence stack on one temp-file SQLite database, wired the way a host wires it (AddApplication() plus AddInfrastructure, a file database because each scope opens its own connection, MMCA.Common/Tests/Performance/MMCA.Common.LoadTests/Support/LoadStack.cs:25). PagedQueryLoadTests drives EntityQueryService.GetAllAsync against 100,000 seeded rows and asserts exact rows rather than a count (.../PagedQueryLoadTests.cs:13, seeded by .../Support/PagedQueryFixture.cs:16); OutboxThroughputLoadTests drains 10,000 events through the real OutboxProcessor to a counting transport, each delivered exactly once, above a rate floor (.../OutboxThroughputLoadTests.cs:20). LoadResults writes one JSON summary per scenario (.../Support/LoadResults.cs:11). Runs weekly from its own workflow (MMCA.Common/.github/workflows/load-tests.yml:16). |
MMCA.ADC, Conference module (the largest application module)
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.ADC.Conference.Shared.Tests |
9 | Conference DTOs, requests, enums, and DTO/request mappers (the manual-mapping/Mapperly boundary, ADR-001). The DTO tests are deliberately shallow and cheap, pinning the required-value constructor, record equality, and the empty-collection defaults that keep a consumer from null-checking every navigation (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Shared.Tests/Events/EventDTOTests.cs:6). Pure unit tests. |
MMCA.ADC.Conference.Domain.Tests |
36 | The Conference aggregates (Event, Session, Speaker, Room, Category, Question/Answer, Sponsor and now Activity): factory-method Result<T> outcomes, invariants, state transitions, and emitted domain events. SponsorTests is the model for the newer files: a database-generated id stays default after Create, exactly one SponsorChanged added-state event is raised, each length/blank invariant fails on its own, a booth number without the exhibitor flag is accepted and kept rather than silently dropped, and the tier values are pinned in package display order because the public roster groups by tier and reordering the enum would silently reorder the page (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Domain.Tests/Sponsors/SponsorTests.cs:11). The Activity aggregate (the non-session agenda items on the public schedule) follows the same shape, split across a behavior file and a dedicated invariants file (.../Activities/ActivityTests.cs:10, .../Invariants/ActivityInvariantsTests.cs:6), with a builder in .../Builders/ActivityBuilder.cs so the Application suite constructs one the same way. EventCascadeDeletionDomainServiceTests is the one to read for ordering: the cascade must soft-delete sessions, rooms, sponsors and activities before the event, must stop at the first child failure, and must leave the event undeleted when it does, so a partially cascaded delete is never committed (.../Services/EventCascadeDeletionDomainServiceTests.cs:9). Unit tests. |
MMCA.ADC.Conference.Application.Tests |
200 | The command/query handlers for the Conference controllers, validators, navigation populators, the Sessionize import orchestrator + sync strategies (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeHandlerTests.cs:17), and the event/session live-window validation served to the live layer over gRPC (.../Events/EventLiveValidationServiceTests.cs:17, alongside GetPublicSessionFilterHandler and its cross-source filter query, ADR-018). It carries the durable AI-scoring pass (ADR-114) in three files. ScoreEventSessionsInternalCommandHandlerTests pins the handler's contract with the framework's at-least-once processor: it takes a per-event claim and skips rather than waits when another replica holds it (each pass is one paid Anthropic call per session), completes the row on a business refusal because replaying it would only pay for the same refusal again, lets a thrown fault propagate so the framework's backoff and attempt ceiling own the retry, and releases the claim on the way out (.../Sessions/UseCases/DecisionSupport/ScoreEventSessionsInternalCommandHandlerTests.cs:17, ScoreEventSessionsInternalCommandHandler). SessionScoringRunnerTests covers the pass itself, including the resume rule that skips every session scored since the command's request instant, which is what makes a queued duplicate or a retried attempt cheap (.../Sessions/UseCases/DecisionSupport/SessionScoringRunnerTests.cs:491, SessionScoringRunner). ScoreEventSessionsInternalCommandMarkerTests pins the opt-in pipeline markers on ScoreEventSessionsInternalCommand: the scoring feature flag, the session-selection capability, and a 12-minute execution budget that must stay below the handler's 15-minute claim time-to-live so the claim cannot expire mid-pass (.../Sessions/UseCases/DecisionSupport/ScoreEventSessionsInternalCommandMarkerTests.cs:51, MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/ScoreEventSessionsInternalCommandHandler.cs:63). It also carries the sessions-by-speaker filter handler, whose specification resolves the SessionSpeaker join down to an engine-portable ID-list criteria so the speaker pages and speaker dashboard filter server-side instead of pulling the whole catalog (.../Sessions/UseCases/GetSessionsBySpeakerFilter/GetSessionsBySpeakerFilterHandlerTests.cs:16, GetSessionsBySpeakerFilterHandler, §12). A family of public filter handlers (session, session-speaker, session-category-item, event-speaker, speaker, speaker-category-item, room, sponsor, and now activity) all defend the same anonymous-endpoint leak: a junction filter must select only rows whose parent is publicly visible, so a hidden session never leaks its existence through the speakers assigned to it (.../Sessions/UseCases/GetPublicSessionSpeakerFilter/GetPublicSessionSpeakerFilterHandlerTests.cs:18, and its activity twin at .../Activities/UseCases/GetPublicActivityFilter/GetPublicActivityFilterHandlerTests.cs:18, BR-49, §11). ExportEventCalendarHandler covers the whole-schedule .ics endpoint, which is anonymous, so unpublished and unknown events must collapse to the same NotFound (no existence oracle) and a legacy row carrying an unresolvable time zone must degrade rather than fault the request (.../Sessions/UseCases/ExportCalendar/ExportEventCalendarHandlerTests.cs:16). The sponsor and activity use cases are deliberately plain: create/update handlers assert the mapper-failure path, the repository add and the save, and nothing more, because the interesting behavior lives in the aggregate and in the public filter (.../Sponsors/UseCases/CreateSponsorHandlerTests.cs:14, .../Activities/UseCases/CreateActivityHandlerTests.cs:13). Its newest files are convention guards rather than behavior: SessionRoomFilterTests pins the public schedule's Room filter at the Application layer, since Session.RoomId is a real nullable column riding the generic filter pipeline with no dedicated handler, so the feature would break silently if the property were renamed or retyped (an unknown filter key is a validation failure and a mistyped one is ignored at apply time, .../Sessions/SessionRoomFilterTests.cs:15); ConferenceCrudRegistrationTests sweeps the module's CRUD registrations so a new aggregate cannot ship half-wired (.../ConferenceCrudRegistrationTests.cs:32); and BatchAddSessionQuestionAnswersHandlerTests covers the batched answer write behind the organizer feedback screens (.../Sessions/UseCases/BatchAddSessionQuestionAnswers/BatchAddSessionQuestionAnswersHandlerTests.cs:24). Its newest files pin the BR-71 category delete cascade, which only reaches loaded items, so the mock returns the category only when the handler declares the include (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Categories/UseCases/DeleteConferenceCategoryHandlerTests.cs:17); the session-asset update's ownership re-check through the real access service (.../SessionAssets/UseCases/UpdateSessionAssetHandlerTests.cs:21); the erasure of a deleted account's feedback answers (.../Users/IntegrationEventHandlers/UserDeletedFeedbackHandlerTests.cs:16); and a speaker create that mints a fresh Guid rather than persisting Guid.Empty (.../Speakers/UseCases/Create/SpeakerCreateRequestMapperTests.cs:7). Its newest types are the in-file TestSessionUrlModel and TestSessionUrlValidator fixtures behind the session URL validation-rule tests (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/Sessions/Validation/SessionValidationRulesTests.cs:63, :65). The biggest application suite in ADC; fast unit tests with mocked repositories/services. |
MMCA.ADC.Conference.Infrastructure.Tests |
12 | Conference-specific EF configurations, the module DB seeder, the Sessionize HTTP client, and SessionScoringService, the session-scoring service that now drives the framework's governed IChatClient (MMCA.Common.AI) rather than an Anthropic-specific handler, so its request/response shape and its cost counters are asserted through that shared pipeline (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionScoringServiceTests.cs:13). The cross-replica claim and the retry policy around a scoring pass are not tested here: they live on the durable internal command in the Application layer (covered in MMCA.ADC.Conference.Application.Tests, above). What this project adds on the scoring side is the cache eviction the pass performs, pinned to the sessions tag only, because evicting the root conference tag instead emptied events, speakers, rooms, categories and questions onto the Basic-tier database while attendees were browsing (.../Services/OutputCacheSessionScoresCacheEvictorTests.cs:12, OutputCacheSessionScoresCacheEvictor). The scoring evaluation material is deliberately not here: the golden-replay, live-judge and prompt-contract tests sit in their own MMCA.ADC.Conference.Scoring.Evaluation.Tests project (the next row) so the paid, key-dependent tier can be gated on its own, which leaves this project the fast unit coverage of the clients, the scoring response guardrail and the cache evictor (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Services/SessionizeServiceTests.cs, .../Services/SessionScoreResponseGuardrailTests.cs, .../Services/OutputCacheSessionScoresCacheEvictorTests.cs) plus the module's EF configuration and seeder (.../Persistence/ConferenceEntityConfigurationTests.cs, .../Seeding/ConferenceModuleDbSeederTests.cs), whose in-file SampleDataHarness is the newest type (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Infrastructure.Tests/Seeding/ConferenceModuleDbSeederTests.cs:191). Unit suite over faked HTTP handlers. |
MMCA.ADC.Conference.API.Tests |
28 | Conference REST controllers (events, sessions, speakers, rooms, categories, questions/answers, session selection, sponsors, activities), the module's permission grants, and the Conference error-resource localization completeness check (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Localization/ConferenceErrorResourcesTests.cs:15, §27). The controller tests follow one shape, visible in SponsorsControllerTests and repeated by the new ActivitiesControllerTests: success and each failure ErrorType map to their status code, and a successful mutation evicts the entity and conference cache tags while a failed one evicts nothing (.../Controllers/SponsorsControllerTests.cs:27, .../Controllers/ActivitiesControllerTests.cs:27). EntityExportAuthorizationTests is the cross-controller guard that came with ADR-078: an action inherited from EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> carries no knowledge of the derived controller's read scoping, and every Conference read filter is applied in a method body rather than as an attribute, so the test pins that every scoped controller declares its own ExportAsync and that export on the bare-[Authorize] controllers carries its capability (.../Controllers/EntityExportAuthorizationTests.cs:18, §11). ConditionalWriteConventionTests pins wiring rather than behavior: the conditional-write behavior lives in the framework filter (covered in MMCA.Common.API.Tests), so what is worth pinning here is that the event lifecycle transitions keep [SupportsIfMatch] and keep a concurrency-aware request body, since an endpoint that quietly lost either would answer 200 to a caller who explicitly asked to write only against the version they read, with nothing failing to say so (.../Controllers/ConditionalWriteConventionTests.cs:14, ADR-035). Unit tests of the API layer. |
MMCA.ADC.Conference.UI.Tests |
129 | Conference Blazor pages and components: the public event/session/speaker/sponsor/activity detail + filtered list pages, the management CRUD forms and management-route authorization, the organizer feedback dashboards, the speaker dashboard, the session-selection dashboard with its AI-score and speaker-overlap views (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Sessions/Selection/SessionSelectionAiScoresTests.cs:15), the home countdown (.../Pages/Home/ADCHomeTests.cs:20), and the share/QR/add-to-calendar buttons. One cluster is all stale-state guards, the failure class bUnit is uniquely good at catching: SessionDetailRoomCacheTests pins that navigating to a session in a different event refetches the per-event room lookup, since otherwise the page renders the previous event's room names and offers its rooms in the edit picker, while navigating within the same event must reuse the cache (.../Pages/Session/SessionDetailRoomCacheTests.cs:20); SessionSelectionStaleResponseTests covers M48, where the organizer dashboard fires a load per event selection and keeps a fire-and-forget score-polling loop running, and both used to write whatever came back into shared state without checking whether the selection had moved on, so a slow response for the previous event could overwrite the board or paint an error banner over it (a generation counter now supersedes in-flight work on every selection, .../Pages/SessionSelection/SessionSelectionStaleResponseTests.cs:23). The sponsor, activity and speaker-QR pages are the newest: PublicSponsorListTests pins the roster scoping to the current-or-next event, grouping by tier in package order with a sort-then-name order inside a tier, the booth badge for exhibitors, and the sponsorship-packet call to action that must appear when the roster is empty and disappear entirely when no packet URL is configured (.../Pages/Public/PublicSponsorListTests.cs:18); PublicActivityListTests does the same for the activity strip on the public schedule (.../Pages/Public/PublicActivityListTests.cs:16); and SpeakerQrTests pins that with the speaker_id claim the code encodes the absolute public profile URL (an app-relative route is useless to another device's camera) and that without the claim the page says so rather than rendering a code that points nowhere (.../Pages/Speaker/SpeakerQrTests.cs:17, ADR-071). Its newest technique is golden-markup: ComponentsSnapshotTests renders each reused Conference component from parameters alone and diffs its normalized markup against a committed baseline under Snapshots/, normalizing the per-render GUIDs MudBlazor injects so the comparison stays deterministic and OS-independent, and refreshed with UPDATE_SNAPSHOTS=1 after an intentional change (.../Components/ComponentsSnapshotTests.cs:27, §28). Alongside it, EventFilteredListPagePrerenderTests pins the prerender pass of the shared filtered-list page base (.../Pages/Common/EventFilteredListPagePrerenderTests.cs:20) and ClientUrlValidationTests pins the client-side URL validation the share and external-link surfaces depend on (.../Pages/ClientUrlValidationTests.cs:23). The newest files pin the shared privileged-reader check of the four public pages, where an auth fault degrades to the public audience but a cancellation propagates (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Public/PublicReadAudienceTests.cs:16); the My Schedule view's inline error and retry on a failed bookmark read (.../Pages/Public/Sessions/PublicSessionListMyScheduleTests.cs:21); that only the unfiltered programme overwrites the offline snapshot (.../Services/PublicSessionScheduleServiceTests.cs:15); the organizer session list's current-event default read from the injected TimeProvider at the local-midnight boundary (.../Pages/Sessions/SessionListCurrentEventClockTests.cs:23); and the client-side BR-122 schedule check (.../Pages/Sessions/SessionDetailScheduleTests.cs:25); a family of *DetailSavedRefreshFailedTests (activity, category, event, partner, question, sponsor) pins that a save the server committed counts as done even when the follow-up reload fails, leaving edit mode with a "saved, but not refreshed" warning and re-reading the row before the next edit so the next save posts the current row version (.../Pages/Activities/ActivityDetailSavedRefreshFailedTests.cs:20); and the public speaker list keeps one card per speaker id when offset paging repeats a row across two page fetches (.../Pages/Public/Speakers/PublicSpeakerListDuplicateAppendTests.cs:20), plus create/edit model and UI-service tests for rooms, sessions, speakers and session assets. Newer pins: ADCHomeTicketingPhaseTests shows the hero and workshop ticket buttons only while the featured event is still ahead, never while it loads, and drops them at the live flip without a reload, over a FakeTimeProvider pinned around a fixed 2030 edition (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.UI.Tests/Pages/Home/ADCHomeTests.cs:227); PublicSpeakerListSupersededLoadTests pins that two overlapping resets of the public speaker list end on the newest reset's rows and never paint "No speakers found." on the way there (.../Pages/Public/Speakers/PublicSpeakerListSupersededLoadTests.cs:22); and NoRecordsContentRecorder with RecordingComponent are fixtures in the session-list load-failure tests (.../Pages/Public/Sessions/PublicSessionListEventsLoadFailedTests.cs:354, :362). Rendered with bUnit (BunitTestBase over the shared BunitComponentTestBase). Component tests. |
MMCA.ADC.Conference.IntegrationTests |
47 | Boots the Conference service host via WebApplicationFactory<Program> (gRPC peers faked, JWT re-pointed at an in-process test key) and drives real HTTP per role (Anonymous/Attendee/Speaker/Organizer), plus OpenAPI contract-snapshot, API-versioning, optimistic-concurrency, soft-delete + audit-stamp fidelity, idempotency replay, output-cache eviction, the includeChildren regression, and the in-process CrossServiceUserRegisteredTests (the Identity-to-Conference UserRegistered auto-link handler). SpeakerFeedbackAuthTests is a plain authorization matrix and a good example of what this tier is for: it covers GET /Speakers/{speakerId}/sessions/{sessionId}/feedback, a speaker's own read of free-text comments gated self-or-organizer, written as the regression for a finding where the endpoint was briefly [AllowAnonymous] with a public output cache, so any caller could read (and publicly cache) any speaker's feedback by URL manipulation (MMCA.ADC/Tests/Integration/MMCA.ADC.Conference.IntegrationTests/Speaker/SpeakerFeedbackAuthTests.cs:15, §11). Its output-cache pair asserts eviction behaviorally, by the changed response body rather than by reading the cache store, and also pins that an authenticated read carries an Authorization header so the default policy skips the cache entirely (.../Reads/OutputCacheEvictionTests.cs:25). Integration style; needs a real SQL Server (ADC_TEST_SQL_BASE), runs in the integration-tests CI job. |
MMCA.ADC.Conference.Scoring.Evaluation.Tests |
10 | The AI-evaluation suite for session scoring, in its own project so that the paid, key-dependent half can be gated apart from CI. A JSON corpus under Golden/ (seven recorded cases plus prompt-versions.json) feeds three files. GoldenReplayTests replays every case through the real SessionScoringService against a response recorded for that case, using a fresh replay client per case via the shared GoldenReplayTestsBase/ReplayChatClient harness, so no API key and no network are involved and it runs on every CI leg; it pins both directions at once, what goes out (the delimited envelope and the untrusted-input brief, including on the no-speakers case and the injection attempt) and what comes back (a recorded response still parses, still succeeds, and still produces the same weighted overall inside the band the case declares), so a change to prompt assembly or to the weighting math fails with the case that noticed it named (MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Scoring.Evaluation.Tests/GoldenReplayTests.cs:35). LiveJudgeTests scores the same proposals through the real Anthropic API, which is what catches the failures a replay cannot (a model deprecation, a structured-output contract change, a prompt edit that reads fine but scores everything a point lower); it costs money and needs a key, so it is trait-gated (Category=AiEval.Live) and skips itself when ANTHROPIC_API_KEY is absent, and its bands are deliberately generous (roughly plus or minus 1.5 around the recorded value, clamped to the 1.0-10.0 range) because a judge model is not deterministic and a tight band would produce a flaky gate that gets ignored (.../LiveJudgeTests.cs:27, §31). PromptContractTests renders the system brief plus the user message for one canonical proposal fixed in the test file (not read from the corpus, so editing a case cannot move what the hash covers), hashes it, and compares against Golden/prompt-versions.json: a prompt edit without a version bump fails, and a version bump with no recorded hash fails too, which is what makes the PromptVersion persisted next to every score mean anything (.../PromptContractTests.cs:29). The corpus record and its nested input/speaker/expectation shapes are in GoldenCase.cs:18,60,76,92, and TestMeterFactory is a real IMeterFactory that exists only because the scoring service records its token counters through one (.../TestMeterFactory.cs:11). Unit (replay) plus a gated live-model evaluation tier. |
MMCA.ADC, Engagement module (bookmarks, feedback, the conference-day live layer, and the badge/points wave)
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.ADC.Engagement.Shared.Tests |
6 | Bookmark/feedback/live DTOs, requests, and mappers, plus the contract types the ADR-072 wave added. BadgePayloadTests pins the QR payload both ways: the mmca-adc:badge:{credential} format, a case-insensitive parse that also accepts a bare or dash-less GUID and tolerates surrounding whitespace (a camera hands over whatever it read), and a clean false plus an empty credential for untrusted garbage (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Shared.Tests/CheckIns/BadgePayloadTests.cs:6). PointsSubjectKeysTests pins the ledger's subject keys, whose whole job is to not collide: event, session and sponsor keys built from the same numeric id must stay distinct, and each must be invariant-culture stable, because the key is a persisted uniqueness constraint and a culture-dependent number format would silently create a second awardable subject (.../Points/PointsSubjectKeysTests.cs:7). The settings and scope-name types round out the row. Unit. |
MMCA.ADC.Engagement.Domain.Tests |
12 | The UserSessionBookmark, event/session feedback, and conference-day live-layer aggregates (LivePoll + SessionQuestion), joined by the badge/points aggregates: factory Result<T> outcomes, invariants, and domain events. CheckIn is the richest, because one aggregate carries three scopes and the scope decides which optional id is required and which is forbidden: a session scope needs a session id, an event scope must not carry one, a sponsor scope needs a sponsor id and no session, and exactly one AttendeeCheckedIn event is raised carrying (or deliberately not carrying) the sponsor payload (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Domain.Tests/CheckIns/CheckInTests.cs:8). AttendeeBadge pins the opaque credential: Create generates one, two calls for the same user never produce the same credential, Regenerate replaces it while preserving the owner, and no domain event is raised either way, since issuing a badge is not an engagement act (.../Badges/AttendeeBadgeTests.cs:6). LeaderboardOptIn pins the display-name snapshot taken at opt-in, the soft-delete leave, and the reactivate path that refreshes the name and raises the added state again, while a failed invariant leaves the entity untouched and raises nothing (.../Points/LeaderboardOptInTests.cs:8); PointsEntryInvariantsTests covers the ledger row's guards field by field (.../Points/PointsEntryInvariantsTests.cs:7). Unit. |
MMCA.ADC.Engagement.Application.Tests |
73 | Bookmark, feedback, and live-layer (poll / session-question) add/remove/query handlers and validators, including the cross-module ISessionBookmarkValidationService / IBookmarkCountService / IEventLiveValidationService gRPC collaborators (stubbed), the poll-results builder (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/Services/LivePollResultsBuilderTests.cs:24), the read-side poll handlers added with the presenter surface (.../LivePolls/UseCases/GetPollResultsHandlerTests.cs:20), the question-upvote domain-event handler (.../SessionQuestions/DomainEventHandlers/SessionQuestionUpvoteChangedHandlerTests.cs:25), and the best-effort ILiveChannelPublisher ingress together with the queue that decouples it from the request path (.../Live/LiveChannelPublishQueueTests.cs:10, LiveChannelPublishQueue). The ADR-072 wave contributes three load-bearing files. PointsAwarder is where anti-farming and redelivery idempotency turn out to be the same rule: an award already present writes nothing, a rule configured to 0 (or an undefined activity) succeeds and writes nothing, an entry keeps the value it was awarded when the configuration later changes, and a save that loses the unique-index race reports success, including when the duplicate wording is buried in an inner exception, while any other save failure propagates (.../Points/Services/PointsAwarderTests.cs:13). CheckInAttendeeHandler pins the scan path: a repeat scan reports the original check-in and writes nothing, a session-scoped scan is filed under the session's own owning event (not the client's event context), an unknown and a malformed credential return the same BadgeNotFound (no probing oracle), and an unpublished event or a missing caller is refused (.../CheckIns/UseCases/CheckInAttendeeHandlerTests.cs:17). GetLeaderboardHandler pins the board as opt-in only (points without an opt-in never appear, and leaving removes you), ordering by total with an ordinal name tie-break, truncation to the configured size where 0 or a negative publishes nothing rather than throwing, and the snapshotted name being what is published (.../Points/UseCases/GetLeaderboardHandlerTests.cs:12). UserDeletedPointsHandlerTests closes the privacy loop: on UserDeleted the entry comes off the board and the published name is erased even for an already-left opt-in, the read deliberately looks past the soft-delete filter and tracks, and a redelivery reaches the same state while writing once (.../Points/IntegrationEventHandlers/UserDeletedPointsHandlerTests.cs:14, §30). Its newest file is the cross-service cache edge: bookmark counts are owned here but served by Conference, in another process whose output cache this side cannot touch directly, so UserSessionBookmarkCacheEvictionHandlerTests pins the one thing that closes that distance, that every bookmark change publishes an eviction request carrying the tag Conference actually registered, and that a broker which is down still cannot fail a bookmark the attendee already saved (.../UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandlerTests.cs:20, ADR-026). The poll delete's soft-delete cascade only reaches loaded options, so the mock returns the poll only when the handler declares the include (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/LivePolls/UseCases/DeleteLivePollHandlerTests.cs:21), and four UserDeleted...HandlerTests pin account erasure of badges, bookmarks, session questions and votes (.../UserSessionBookmarks/IntegrationEventHandlers/UserDeletedBookmarksHandlerTests.cs:14). Unit. |
MMCA.ADC.Engagement.Infrastructure.Tests |
8 | Engagement EF configuration plus the live-channel publish processor that fans domain changes out to the SignalR hub (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure.Tests/Live/LiveChannelPublishProcessorTests.cs:10). Unit. |
MMCA.ADC.Engagement.API.Tests |
12 | The Bookmarks/Feedback/Live/CheckIns/Points REST controllers in isolation. CheckInsControllerTests shows the two rules that matter at this edge: GetMyBadgeAsync takes no parameter other than the cancellation token, so the identity can only come from the token and never from the request body (asserted by reflection, because adding a parameter would break nothing loudly), and a repeat scan is Ok, not Conflict, because the organizer scanning a badge twice is a normal event and an error status would train them to ignore it (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.API.Tests/Controllers/CheckInsControllerTests.cs:23). ConditionalWriteConventionTests does for Engagement what its Conference twin does for events, on the endpoints two people actually race in a live room: poll open/close and the three question moderation decisions, where an endpoint that quietly lost [SupportsIfMatch] would let a moderator's explicit precondition pass unenforced (.../Controllers/ConditionalWriteConventionTests.cs:13, ADR-035). Its newest file pins the owner-or-admin filter vocabulary, the exact policy and claim names the ownership filters are constructed with, because a typo there fails open rather than loud (.../Authorization/OwnerOrAdminFilterVocabularyTests.cs:23, §11). Unit. |
MMCA.ADC.Engagement.UI.Tests |
54 | Engagement Blazor renders and their UI services: the bookmark UI, the session/event feedback pages (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Pages/Feedback/SessionFeedbackTests.cs:26), the conference-day live/presenter surfaces (Happening Now, live poll, session Q&A, the moderation panel), the live-channel join/reconnect path (.../Pages/LiveChannelJoinTests.cs:40), and the session-reminder planner/coordinator. LiveEventListenerResilienceTests covers H18, where an invisible layout component rendered on every authenticated page sits under a shell with no ErrorBoundary above it, so anything escaping its render lifecycle tears down the Blazor circuit (a full page reload) on whatever page the user happens to be on, and the shared resilience handler raises TimeoutRejectedException/BrokenCircuitException that the service layer's narrow HttpRequestException catch deliberately does not cover (.../Components/LiveEventListenerResilienceTests.cs:34, §29); NowNextService covers the anonymous now-next endpoint contract plus its not-found degradation. The badge/points surfaces are the newest. CheckInScanTests is the load-bearing one: on a head whose IBarcodeScannerService.IsSupported is false (web and Windows) the scan affordance must not render at all while the manual name/email search stays, because that search is the whole check-in surface there, and the scope toggle must default to Session (.../Pages/CheckIn/CheckInScanTests.cs:20, ADR-042 / ADR-072). MyBadgeTests and MyPointsTests pin the four-state render (loading, data, empty, error) for the attendee's own badge and ledger, including the leaderboard rank block and the participation switch reflecting the opt-in state on load (.../Pages/CheckIn/MyBadgeTests.cs:19, .../Pages/Points/MyPointsTests.cs:19), SponsorVisitTests pins that a repeat visit reports the earlier one rather than an error, with distinct states for an unpublished event, an unrecognized server answer, and a failed post (.../Pages/Sponsors/SponsorVisitTests.cs:19), and the organizer rollups plus the room-code page have their own files (.../Pages/CheckIn/OrganizerAttendanceTests.cs:17, .../Pages/Rooms/RoomCheckInTests.cs:18). CurrentEventNotificationScopeProvider covers the event:{EventId} key shape, the never-throw degradation to unscoped, and the five-minute cache that keeps the notification bell's 30-second poll from costing an events fetch every time (.../Services/CurrentEventNotificationScopeProviderTests.cs:15, §31). Its newest three are the check-in service the scan pages call (.../Services/CheckInServiceTests.cs:15), the app-action route map that turns a notification payload into a destination route (.../Services/AppActionRouteMapTests.cs:20), and a golden-markup snapshot suite over the live-room components, where the committed baselines under Snapshots/ cover the poll card, the moderation panel, the question panel and the attendee search panel (.../Components/ComponentsSnapshotTests.cs:35, §28). LiveEventListenerTests pins that the circuit-lifetime listener still joins the live channel when the event's live window opens after the shell first rendered, not only when the event is live at the first render (MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.UI.Tests/Components/LiveEventListenerTests.cs:27), and PresenterLayoutTests pins that the projector layout, which replaces MainLayout entirely, renders the layout components the UI modules register itself, so a deep link opened while the presenter view is on screen is not left buffered and the app-lock re-arm still runs there; its marker components and LayoutComponentModule are in-file fixtures (.../Layouts/PresenterLayoutTests.cs:21, :46). Component (bUnit). |
MMCA.ADC.Engagement.IntegrationTests |
22 | Boots the Engagement service host via WebApplicationFactory<Program> and exercises the bookmark/feedback/live workflows + authorization over real HTTP, with the badge/points tier alongside. CheckInScanRoundTripTests proves over a real database what the handler tests prove in memory: a second scan reports the repeat and leaves one row and one enqueued AttendeeCheckedIn, a stale event context is corrected to the session's own event, and MyBadge returns the same credential on every call (MMCA.ADC/Tests/Integration/MMCA.ADC.Engagement.IntegrationTests/CheckIns/CheckInScanRoundTripTests.cs:24). PointsAwardRoundTripTests is the one to read for the outbox idiom: the check-in and its event commit in one transaction, so each test then drains the outbox the way OutboxProcessor does, reading the persisted payload back out of dbo.OutboxMessages, deserializing it, and handing it to the registered handler, which keeps the assertions deterministic instead of racing a background service, and it pins that a redelivery still leaves exactly one entry and that the unique index rejects a second award for the same subject (.../Points/PointsAwardRoundTripTests.cs:41). CheckInAuthorizationTests states the authorization contract plainly: every attendee may fetch their own badge (identity from the token, never the request), while writing a check-in and reading the attendance rollup require engagement:checkin:manage, held only by Organizer and Admin, and anonymous callers get 401 everywhere (.../CheckIns/CheckInAuthorizationTests.cs:14, §11). Integration; real SQL Server, integration-tests CI job. |
MMCA.ADC, Identity module (User aggregate + JWT/JWKS + external OAuth)
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.ADC.Identity.Shared.Tests |
2 | Two Shared-project contracts: IdentityPermissionGrantsTests pins the Identity module's effective role-to-permission map (ADR-020) with literal expected sets, so a change to how a grant is declared can never change what a role is granted (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Shared.Tests/Authorization/IdentityPermissionGrantsTests.cs:14), and DisabledAttendeeQueryServiceTests checks the disabled attendee query service returns an empty list without throwing (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Shared.Tests/Users/DisabledAttendeeQueryServiceTests.cs:6). The JWT audience guard tests now live in MMCA.Common.API.Tests (MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Startup/Auth/JwtAudienceTests.cs:12). Unit. |
MMCA.ADC.Identity.Domain.Tests |
6 | The User/UserRole aggregate factories, invariants, anonymization, and speaker-linking domain events. Unit. |
MMCA.ADC.Identity.Application.Tests |
36 | Registration/login/profile/role/preferences handlers and validators, the external-OAuth (Google/GitHub) exchange, the SpeakerLinkedToUser/SpeakerUnlinkedFromUser integration-event handlers, and the attendee query service the Notification module reads recipients through. The data-subject export block (ADR-076 / §30, ADC still runs its own handler rather than the framework's endpoint): ExportUserDataHandlerTests pins the owner-or-organizer gate with a case-insensitive role claim, the aggregation of the Engagement and Notification sections when the peers answer, the per-section unavailable degradation when a peer is unreachable or throws so the rest of the package still travels, and UTC-kind timestamps out of DateTimeKind.Unspecified audit columns (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Application.Tests/Users/UseCases/ExportUserData/ExportUserDataHandlerTests.cs:16); EngagementUserDataExportSectionTests pins what the Engagement section actually carries, including bookmarks, submitted questions, points entries with the readable activity name, leaderboard participation, and check-ins that earned no points (a check-in is personal data whether or not it scored, .../Users/UseCases/EngagementUserDataExportSectionTests.cs:11); and ExportUserDataRegistrationTests pins that the handler is discovered through its base class interface by the module scan and that section registration order is preserved (.../Users/UseCases/ExportUserDataRegistrationTests.cs:16). Its newest files are the app-side halves of two framework waves: the password-recovery handlers over the shared bases (.../Users/UseCases/ForgotPasswordHandlerTests.cs:22, .../Users/UseCases/ResetPasswordHandlerTests.cs:19, ADR-091), an in-memory refresh-session store used as the test double for the multi-device surface (.../Support/InMemoryRefreshSessionStore.cs, ADR-097), and the avatar read/remove handlers (.../Users/UseCases/GetUserAvatarHandlerTests.cs:15, .../Users/UseCases/RemoveUserAvatarHandlerTests.cs:16, ADR-045). Unit. |
MMCA.ADC.Identity.Infrastructure.Tests |
12 | Identity EF config/repository, RS256 token issuance, and the JWKS provider. Its newest file is RefreshSessionModelGateTests, the consumer-side proof that the framework's refresh-session table appears in this host's model only when the feature is enabled, so a host that never opted in keeps exactly the migrations it had (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Persistence/RefreshSessionModelGateTests.cs:46, ADR-097). SkiaSharpImageProcessor, the avatar normalizer, has its own file: untrusted uploads come out as exact-size square JPEGs, oriented, center-cropped to cover and with all metadata stripped (EXIF GPS is PII), only JPEG, PNG, WebP and GIF are accepted, and anything else, or a header declaring an oversized frame, fails as a validation error rather than an exception (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.Infrastructure.Tests/Services/Users/SkiaSharpImageProcessorTests.cs:14, ADR-045), with ImageFixtures and NonSeekableStream as its fixtures (.../Services/Users/ImageFixtures.cs:11, .../Services/Users/SkiaSharpImageProcessorTests.cs:382). Unit. |
MMCA.ADC.Identity.API.Tests |
11 | The Auth REST controller, the Users controller, the JWKS endpoint, and identity middleware in isolation, plus the data-export controller that fronts ADC's own DSAR handler (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.API.Tests/Controllers/UsersDataExportControllerTests.cs:20, §30). Unit. |
MMCA.ADC.Identity.UI.Tests |
18 | Identity Blazor pages (login/register/profile/user-management) rendered with bUnit, now with a golden-markup snapshot suite over the user-claims component (MMCA.ADC/Tests/Modules/Identity/MMCA.ADC.Identity.UI.Tests/Components/ComponentsSnapshotTests.cs:15, §28). Component. |
MMCA.ADC.Identity.IntegrationTests |
40 | Boots the Identity service host via WebApplicationFactory<Program> and drives the full auth surface over real HTTP: registration, login and its anonymous edge cases, claims, profile, user preferences, soft-deleted-user handling, the external-OAuth challenge/exchange, GDPR user export against faked Engagement and Notification peers (MMCA.ADC/Tests/Integration/MMCA.ADC.Identity.IntegrationTests/Attendee/UserExportTests.cs:24), and JWKS discovery. It also carries the two contract guards (OpenAPI snapshot and the RFC 9457 Problem Details subclass over ProblemDetailsContractTestsBase<TFixture>, .../Contract/ProblemDetailsContractTests.cs:16, §9), the compliance pair that proves erasure works end to end and that PII never reaches the log pipeline (.../Compliance/ErasureAndPiiLoggingTests.cs:19, ADR-005 / §30), an outbox-fidelity guard asserting registration atomically enqueues UserRegistered into [dbo].[OutboxMessages] (.../Data/OutboxFidelityTests.cs:17), the in-process CrossServiceSpeakerLinkTests, and the newest addition, PasswordResetFlowTests, which walks forgot-then-reset end to end against a real database and a real token store so the anti-enumeration answers and the single-use token are proved where they actually ship (.../Auth/PasswordResetFlowTests.cs:18, ADR-091). Integration; real SQL Server, integration-tests CI job. |
MMCA.ADC, Notification module (push + inbox on top of the framework's notification types)
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.ADC.Notification.API.Tests |
1 | NotificationModuleTests pins the module contract itself, and it is a five-member subclass of the shared ModuleConformanceTestsBase<TModule>: it declares only ExpectedName ("Notification"), ExpectedDependencies (["Identity"]), ExpectedRequiresDependencies (true), and an AssertDisabledStubs override that proves RegisterDisabledStubs keeps the cross-module IUserNotificationExportService resolvable as a singleton DisabledUserNotificationExportService when the module is switched off (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.API.Tests/NotificationModuleTests.cs:8). The conformance assertions themselves live in the base. Unit. |
MMCA.ADC.Notification.Application.Tests |
4 | The module's two application services plus its DI registration: AttendeeNotificationRecipientProvider resolving broadcast recipients through the Identity IAttendeeQueryService gRPC contract (MMCA.ADC/Tests/Modules/Notification/MMCA.ADC.Notification.Application.Tests/AttendeeNotificationRecipientProviderTests.cs:7), UserNotificationExportService assembling a data-subject export from the user-notification and push-notification repositories over InMemoryQueryableExecutor on top of HandlerTestBase<THandler> (.../UserNotificationExportServiceTests.cs:12, §30), and AddModuleNotificationApplication proving both are registered against their interfaces (.../DependencyInjectionTests.cs:10). Unit. |
MMCA.ADC.Notification.IntegrationTests |
10 | Boots the Notification service host via WebApplicationFactory<Program> (the Identity recipient-lookup gRPC client faked by FakeAttendeeQueryService) and exercises the push-notification REST endpoints + inbox (NotificationsController/InboxController from MMCA.Common.API, MMCA.ADC/Tests/Integration/MMCA.ADC.Notification.IntegrationTests/Notifications/NotificationControllerTests.cs:16), the two contract guards (an OpenAPI snapshot at .../Contract/OpenApiContractTests.cs:16 and the Problem Details subclass at .../Contract/ProblemDetailsContractTests.cs:15, §9), and the real-time SignalR NotificationHub: a live HubConnection asserts authenticated connect, anonymous rejection (the hub carries [Authorize]), and a POST-triggered broadcast reaching the connected recipient (.../Notifications/NotificationHubTests.cs:15). Integration; real SQL Server (ADC_TEST_SQL_BASE), integration-tests CI job. |
MMCA.ADC, host, service-adapter, cross-service, and end-to-end suites
| Test project (assembly) | Types | What it covers · style |
|---|---|---|
MMCA.ADC.Gateway.Tests |
11 | Boots the real YARP Gateway host in-process and asserts its operational guarantees, most of them with no test body in this repo at all: the host fixture and the gate bodies come from MMCA.Common.Testing, so ADC supplies only the entry point and its own route-table facts. SecurityHeadersTests is a four-line subclass of SecurityHeadersTestsBase that hands it a client from the shared ProductionHostApplicationFactory<Program> class fixture (MMCA.ADC/Tests/Hosts/MMCA.ADC.Gateway.Tests/SecurityHeadersTests.cs:12); the base probes /alive, which always answers regardless of backend reachability, and pins X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, frame-ancestors 'none' and HSTS (§26). GracefulShutdownTests is a body-less subclass of GracefulShutdownTestsBase<TEntryPoint> closed over the Gateway's Program (.../GracefulShutdownTests.cs:9): the base boots the host, stops it under a bounded token, and asserts ApplicationStopping then ApplicationStopped fired, so a hosted service that refuses to drain fails here instead of silently wedging a rolling deploy (§29). RouteMapTests is the one to read. Program.cs no longer hand-maps forwarders: it loads the ReverseProxy configuration section and applies the MMCA.Common.Gateway config filters over it (ADR-089), so adding or repointing a route is an appsettings.json edit. A config-owned table can drift in two directions, so the suite has two gates over one pinned table (.../RouteMapTests.cs:118). Behavioral: every pinned route is driven through the real proxy pipeline with IHttpForwarder replaced by RecordingHttpForwarder, which echoes the destination prefix, the activity timeout, and the forwarded HTTP version and version policy into response headers, so a removed or repointed route, a dropped timeout or a lost h2c setting fails there (.../RouteMapTests.cs:159), and an unmapped path must 404 at the gateway rather than be swallowed by a catch-all (:370). Completeness: the loaded IProxyConfig is compared against the same table, routes (:192) and clusters (:214), so a route ADDED to configuration without a test fails too. The table today is 27 routes across 5 clusters: four identity, sixteen conference, five engagement, two notification. Five clusters rather than four precisely because a YARP cluster owns the request config, so the REST 100-second activity timeout (MMCA.Common's shared 90-second request budget plus a deliberate 10-second margin, so the forwarder outlives the backend's own budget and the client sees the backend's error rather than a gateway abort, .../RouteMapTests.cs:81), the SignalR hub's long timeout, and the h2c prior-knowledge version pin on the three Http2-only backends cannot share one; the REST budget is now declared once as MmcaGateway:ClusterRequestDefaults instead of copied into five HttpRequest blocks, and the test asserts the effective cluster picks it up (:250). Two newer facts live here too: /Auth/{**catch-all} carries a tighter named limiter (auth-tight) than the edge global one, because everything under /Auth is credential handling and a burst from one IP there is far more likely to be an attack than a user, and every other route is asserted to carry no named policy (:307, :69-72); and every proxied request is stamped with route and cluster trace headers (:327). GatewayHardeningTests is now a subclass of MmcaGatewayHardeningTestsBase<TEntryPoint> that supplies only ADC's numbers: the per-client-IP allowance of 120 (.../GatewayHardeningTests.cs:38), the tighter named policy at 30 (:80), a representative limited path, and the downstream service names, while the eight gates themselves (bypass list, correlation ID generated or echoed, partitioning by the forwarded client IP, one readiness check per downstream service tagged ready and never live) live once in the base (ADR-088). AppHostBicepParityTests is the newest and guards a boundary nothing else does: Aspire's WithReference(peer) injects services__{peer}__{endpoint}__0 environment variables and infra/main.bicep hand-writes the same variables onto each container app, with nothing connecting the two, so a new cross-service call boots locally, passes every test and then fails in production with an unresolvable service name (and a Bicep entry for a removed edge is dead configuration nobody notices); both files are read as text rather than executed, because the AppHost hangs when started headless and Bicep is not executable here at all (.../AppHostBicepParityTests.cs:24, §17). The Gateway is a pure reverse proxy (no DbContext or broker) so the boot needs no SQL. Integration style. |
MMCA.ADC.Services.Tests |
23 | The only project that tests the service-host gRPC adapters directly rather than through a booted host. Its first subject is an easily-missed wire contract in the data-subject export named by ADC's private PRIVACY.md §7 (ADR-005 / §30): SQL Server returns DateTimeKind.Unspecified values, and the "O" format omits the Z marker for them, so a timestamp that is genuinely UTC crosses the wire looking like a local time. UserEngagementExportGrpcServiceTests and UserNotificationExportGrpcServiceTests assert the emit leg (every timestamp ends with Z and parses back to the same instant, MMCA.ADC/Tests/Services/MMCA.ADC.Services.Tests/Exports/UserEngagementExportGrpcServiceTests.cs:17); the two ...GrpcAdapterTests assert the parse leg, which has to survive a rolling deploy where new Identity code talks to a Notification replica still emitting the marker-less form, so both wire forms must land as DateTimeKind.Utc on identical ticks (.../Exports/UserNotificationExportServiceGrpcAdapterTests.cs:19). FakeServerCallContext is a minimal ServerCallContext that lets a server implementation be invoked directly: the export services read only the cancellation token, so nothing else has to behave (.../Support/FakeServerCallContext.cs:10). The rest round-trip each cross-module gRPC server through its client adapter, resolved via the public composition with the typed client mocked (GrpcCalls builds the call a mocked client returns, MMCA.ADC/Tests/Services/MMCA.ADC.Services.Tests/Helpers/GrpcCalls.cs:9): attendee ids keep their order (.../Attendees/AttendeesGrpcServiceTests.cs:23), bookmark counts and bookmark validation survive the wire (.../Bookmarks/BookmarkCountsGrpcServiceTests.cs:17, .../Bookmarks/SessionBookmarksGrpcServiceTests.cs:18), and the adapters turn an RpcException or unmappable wire data into a failure Result rather than an escaping exception (.../Bookmarks/SessionBookmarkValidationServiceGrpcAdapterTests.cs:23, .../Live/EventLiveValidationServiceGrpcAdapterTests.cs:24, .../Live/EventLiveValidationGrpcServiceTests.cs:23). The anonymous live-channel ingress refuses a malformed key or event name or an oversized payload with InvalidArgument before the SignalR publisher (.../Live/LiveChannelGrpcServiceTests.cs:27), its publisher adapter is best-effort and swallows a failed push (.../Live/LiveChannelPublisherGrpcAdapterTests.cs:16), and the Conference warm-up's literal paths are pinned to PagedReadAll.LookupPageUrl so a warmed OutputCache key is one the UI actually requests (.../Warmup/SelfHttpOutputCacheWarmupTaskTests.cs:15); the Identity warm-up pins each path to a GET template on UsersController, because a path routing does not match answers 404 before the authentication, authorization and controller pipeline is JIT-compiled and RequireSuccessStatusCode = false hides the miss (.../Warmup/IdentitySelfHttpWarmupTaskTests.cs:15). The Conference half of the data-subject export is covered on both sides of the wire: the gRPC service puts the user's authored feedback answers on the wire with UTC-marked timestamps (.../Exports/UserConferenceExportGrpcServiceTests.cs:15), and Identity's adapter lands both timestamp forms as DateTimeKind.Utc and lets a transport fault propagate so the export handler can mark the section unavailable (.../Exports/UserConferenceExportServiceGrpcAdapterTests.cs:18). In MMCA.ADC.CI.slnf (MMCA.ADC/MMCA.ADC.CI.slnf:62), so it runs on every PR with no DB. Unit style. |
MMCA.ADC.CrossService.IntegrationTests |
13 | The real-broker + real-gRPC tier: boots the REST hosts in one process against a Testcontainers SQL Server and a Testcontainers RabbitMQ, so the genuine MassTransit outbox to broker to consumer round-trip (UserRegistered auto-link, SpeakerLinked/SpeakerUnlinked back-link) and the real Conference to Engagement bookmark-count gRPC read run end to end, over a sequential env-boot fixture (a subclass of the shared CrossServiceFixtureBase) and a smoke gate that fails first if the container/host wiring is wrong. Its newest test is the one that needs this tier most: TwoReplicaHubFanOutTests boots the Notification host twice and proves a notification published on one replica reaches a SignalR client connected to the other, which is precisely the guarantee a single-host integration test cannot observe (MMCA.ADC/Tests/Integration/MMCA.ADC.CrossService.IntegrationTests/CrossService/TwoReplicaHubFanOutTests.cs:35, §7), supported by a Notification-specific factory and a faked attendee query service in .../Infrastructure/. Integration style; needs Docker, runs in the weekday-nightly cross-service job (MMCA.ADC/.github/workflows/cross-service-tests.yml:75, scheduled by the 0 6 * * 1-5 cron at .../cross-service-tests.yml:31, behind a should-run guard that skips a night with no new commits, .../cross-service-tests.yml:50), not in Integration.slnf. This is one of the two jobs whose recency the cross-service-freshness deploy gate keys off. |
MMCA.ADC.ServiceBusEmulator.IntegrationTests |
3 | Broker-parity smoke (§33): production runs on Azure Service Bus while local development runs RabbitMQ, so Service-Bus-specific transport behavior is otherwise observable only in the deployed environment. This tier runs MassTransit v8 against the official Service Bus emulator container with ADC's real integration-event contracts and proves the two transport-specific behaviors: admin-plane topology provisioning (topic per message type, subscription, receive-endpoint queue) and the AMQP publish to topic to subscription to consume round-trip (MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/ServiceBusRoundTripSmokeTests.cs:26). The fixture is the whole lesson here. It was dispatch-only from 2026-07-24 after hanging and being killed at its timeout on 7 of 7 runs, and the cause was read at the time as the emulator's floating companion SQL image; it was not. The real cause was provisioning volume: the test class implemented IAsyncLifetime, xUnit re-instantiates a test class per [Fact], and every bus start re-provisions the whole topology through an admin plane throttled at roughly one operation per second. The bus now lives on a collection fixture and starts once, and both startup phases are wall-clock bounded so a future hang fails with a phase-named error instead of being killed at the job timeout (which discards the step log, and is why this went unlocalized for a week): ServiceBusEmulatorFixture is now a thin subclass of the shared ServiceBusEmulatorFixtureBase (.../Infrastructure/ServiceBusEmulatorFixture.cs:22) with its [CollectionDefinition] at :51. Since 2026-08-31 the servicebus-emulator-smoke job is authoritative rather than advisory: it carries no continue-on-error, runs under a 10-minute spend cap (MMCA.ADC/.github/workflows/cross-service-tests.yml:153), and the cross-service-freshness deploy gate now requires a run in which both it and cross-service concluded success. (The third nightly job, apphost-smoke, is the one that stays advisory under ADR-098, .../cross-service-tests.yml:199.) Integration style; needs Docker. |
MMCA.ADC.E2E.Tests |
97 | Playwright end-to-end against the running Aspire stack, using a Page-Object model (PageObjects/, whose page classes count toward the 96 alongside the workflow classes) and E2ETestBase login helpers, organized by actor workflow (Organizer/Speaker/Attendee/Identity/Preferences) plus the Engagement live-poll, session-Q&A and feedback flows, real-time notification push, a Web-Vitals budget check, a pseudo-locale sweep, and an AccessibilityTests axe sweep that runs 31 WCAG 2.1 AA scans across the public browse pages (events, sessions, speakers, sponsors), the organizer management lists and create forms, the speaker dashboard and QR page, Happening Now as both actors, and the badge/points/check-in/attendance surfaces (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs:29); it complements the Identity-flow scans shipped in the MMCA.Common.Testing.E2E workflow bases. Those badge/points surfaces are no longer axe-only: CheckInAndPointsTests drives the attendee badge, the organizer manual check-in fallback, the attendee-driven room and booth codes, the two organizer rollups, and the ledger every one of them feeds (.../Workflows/Engagement/CheckInAndPointsTests.cs:29). It is also the file to read for two techniques the rest of the suite reuses. Three of its journeys need an event that is LIVE right now and no seeded event ever is (their dates are fixed), so each mints its own same-day published event, rooms and a session spanning the current moment, and DELETES the event in a finally block, because a leftover same-day published event becomes the app-wide current event (home phase, Happening Now, check-in, attendance, notification scope) for every later test in the suite; and the minted event is created in UTC rather than the seeded America/New_York, because session times are stored as event-zone wall clock, so with a UTC event the wall clock IS DateTime.UtcNow and there is no zone conversion to get wrong on a runner in any region. Points are awarded asynchronously (the check-in writes its row, the outbox publishes AttendeeCheckedIn, and the Engagement points handler awards on the way back), so every ledger assertion re-navigates in a bounded loop instead of expecting the first read to show it. The same wave added the attendee share/export journey (.../Workflows/Engagement/AttendeeShareAndExportTests.cs), live session Q&A (.../Workflows/Engagement/LiveSessionQaTests.cs:22), a data-integrity sweep (.../Workflows/Conference/DataIntegrityTests.cs:11), the public sponsor browse and organizer sponsor management flows (.../Workflows/Conference/PublicSponsorBrowseTests.cs:13), and two suites that are pure subclasses of shared bases: AuthorizationTests over AuthorizationTestsBase (.../Workflows/Identity/AuthorizationTests.cs:11) and a body-less PasswordResetTests over PasswordResetTestsBase (.../Workflows/Identity/PasswordResetTests.cs:5). The live-window arrangement the Q&A and moderation journeys share is one helper, LiveEventArrangement, which creates a published event plus a session spanning now and deletes the event afterwards, because a leftover live event becomes the app-wide current event (.../Helpers/LiveEventArrangement.cs:19); LiveModerationKeyboardTests uses it to run the shared KeyboardOperabilityTestsBase over the session Live page and the focus trap and Escape return of the moderation panel's poll-delete dialog (.../Workflows/Engagement/LiveModerationKeyboardTests.cs:23), and PhoneViewportTests asserts that each anonymous attendee page renders its main heading without scrolling sideways at a 390x844 viewport (.../Workflows/Conference/Public/PhoneViewportTests.cs:18). Runs once per engine via E2E_BROWSER (MMCA.ADC/.github/workflows/e2e.yml:301); the chromium leg gates deploy through e2e-gate (MMCA.ADC/.github/workflows/deploy.yml:677) while firefox and webkit alternate on the Monday and Thursday crons (.../e2e.yml:49-50). InstallableWebAppTests pins what this host supplies for the installable web app: a manifest an installing browser accepts (name, standalone display, root start URL, theme color, and 192, 512 and maskable icons that all resolve) and the framework service worker served at the site root as uncached JavaScript (MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Conference/Public/InstallableWebAppTests.cs:13). The largest single project here and the source of most of the chapter's recorded E2E debugging history. [Rubric §28, Front-End Testing] + [Rubric §22, Responsive/Cross-Browser]: this suite is the cross-browser, real-user-flow safety net. E2E style. |
⬅ Common Cloud Provider Adapters (AWS, Azure, GCP) • Index • Coverage audit ➡