Architecture tests turn “how the code should be organized” into an executable merge contract. BitzOrcas.Architecture.Tests does more than inspect assembly references: it parses .csproj, source, JSON, YAML, and architecture documents to stop old mappers, runtime reflection, invalid cross-module references, stale commercial catalogs, and deleted shallow modules from returning.
Position in the evidence chain
These tests prove structure still conforms to accepted design. They do not prove SQL, transaction, or messaging behavior on real infrastructure; integration contracts own that evidence. Conversely, a working API path does not prove dependency direction remains sound.
Current protection surface
| Surface | Typical fact | Primary evidence |
|---|---|---|
| Layering | Domain does not depend on Application/Infrastructure/Api | assemblies, .csproj |
| Module collaboration | cross-module calls use approved Contracts seams | project graph, exception registry |
| Persistence | no ORM in Application; aggregate and metadata rules hold | project XML, source, generated manifest |
| AOT | unregistered runtime-reflection paths remain banned | source scan, trim-gate contract |
| Generators | DI, endpoints, queries, persistence metadata are compile-time outputs | generator source and consumption tests |
| Commercial delivery | catalog, Profile closure, packable projects agree | JSON, project properties, directory scan |
| Consumer | templates do not copy core source or reference the product repository | isolated consumer and source contracts |
| CI | jobs, triggers, timeout, Docker shards, GA fail-closed behavior remain explicit | workflow YAML contract |
| Deletion gates | shallow modules, old mappers, old query reflection cannot return | global path and token sweep |
The repository has risk-focused classes such as CsprojDependencyGraphTests, ReflectionAotGuardTests, CommercialPackageCatalogTests, DockerContractTraitTests, and ShallowModuleDeletionGateTests. Extending an existing owner is easier to maintain than creating a catch-all test class.
Three rule implementations
Compiled dependency rules
Assembly rules answer “what does the produced binary actually reference?” Workflow rules load BitzOrcas.Workflow.Engine and assert that it does not reference SqlSugar, EF Core, Dapper, ASP.NET Core, or a Workflow adapter. That evidence is closer to the deliverable than source using statements.
[Theory][InlineData("BitzOrcas.Workflow.Engine")]public void WorkflowEngine_ShouldNotReferenceAdapters(string assemblyName){ // Inspect direct references in the compiled artifact, not source imports. var references = Assembly.Load(assemblyName) .GetReferencedAssemblies() .Select(item => item.Name) .ToArray();
// Keep forbidden edges explicit so failures identify the broken boundary. references.ShouldNotContain("SqlSugar"); references.ShouldNotContain("Microsoft.EntityFrameworkCore"); references.ShouldNotContain("BitzOrcas.Workflow.EfCore");}Project graph and machine-readable contracts
CsprojDependencyGraphTests parses applicable .csproj files, distinguishes ProjectReference from PackageReference, and reads an explicit exception registry. It catches an Application referencing an ORM package, Contracts pointing back into implementation, or module A depending directly on module B’s Application.
var project = XDocument.Load(projectPath);
// Internal project edges reveal layering; package edges reveal ORM leakage.var projectReferences = project.Descendants("ProjectReference") .Select(node => node.Attribute("Include")?.Value) .Where(value => value is not null) .ToArray();
// This assertion owns Application-to-Infrastructure; separate rules own other layers.projectReferences.ShouldNotContain(path => path!.Contains(".Infrastructure", StringComparison.Ordinal));Source and deletion gates
Some contracts cannot be reconstructed from assemblies: a command must use a transition template, a retired directory must not exist, or CI must retain thirteen Docker shards. Those rules inspect source or workflow files and report full offender paths. Source tokens are appropriate for explicit, stable structure—not for inferring complicated behavior.
Run and narrow the suite
# Run the complete architecture contract before merge.dotnet test tests/BitzOrcas.Architecture.Tests --configuration Release
# Verify only CI quality-gate contracts; class filters are more stable.dotnet test tests/BitzOrcas.Architecture.Tests \ --configuration Release \ --filter 'FullyQualifiedName~CiQualityGateTests'When a rule consumes Release artifacts, do not explain it with stale bin/Debug output. Use --no-build only after building the same commit and configuration.
Add a rule end to end
- State the regression precisely, such as “Platform Application must not PackageReference an ORM,” not “keep architecture clean.”
- Locate the authoritative ADR, constraint, or machine-readable catalog; write the decision first if none exists.
- Choose evidence closest to the fact: assembly, project XML, manifest, source path, or CI YAML.
- Make the test fail against a violating fixture or temporary change so it cannot be permanently green.
- Report every offender, the reason, and the correction path rather than returning only
false. - Register any real exception with an owner, rationale, and exit condition; never allow an entire directory.
- Run the focused rule, all Architecture.Tests, and the affected business tests.
Diagnose failures
Start with the failing class name; it normally identifies ownership. For project-graph failures inspect the offending .csproj and exception registry. For source gates determine whether a path moved or semantics changed. For manifests compare both producer and consumer. Change specification, test, and implementation together only when the architecture decision truly changed.
Review checklist
- Rule names express an architectural fact, not an implementation action.
- Scan scope includes new modules and documents exclusions explicitly.
- Failure output lists every offender and the authoritative rule.
- Windows/Linux separators and case differences are handled.
- Exceptions are exact entries, never permanent directory exemptions.
- Structural tests do not impersonate runtime, performance, or security evidence.
- A global
rg/findsweep follows the local change; expected old-pattern count is zero.