|
| 1 | +# Architecture Testing in .NET — Minimal Layer Rule |
| 2 | + |
| 3 | +A minimal .NET 10 architecture-testing sample demonstrating how a dedicated |
| 4 | +test project inspects compiled assemblies and fails the build when a forbidden |
| 5 | +dependency appears. |
| 6 | + |
| 7 | +## Full tutorial |
| 8 | + |
| 9 | +[Architecture Testing in .NET: Enforce Layer and Module Boundaries with NetArchTest and ArchUnitNET](https://www.dotnet-guide.com/tutorials/software-architecture/architecture-testing-dotnet/) |
| 10 | + |
| 11 | +## What this sample demonstrates |
| 12 | + |
| 13 | +* a layered .NET 10 solution with Domain, Application, and Infrastructure projects; |
| 14 | +* an architecture-test project using **NetArchTest.eNhancedEdition**; |
| 15 | +* typed assembly markers that give the test project a reliable way to locate each assembly; |
| 16 | +* one forbidden-dependency rule that prevents Domain from depending on outer layers; |
| 17 | +* selector non-empty protection so the rule cannot silently pass when no types are found; |
| 18 | +* readable failure output naming the protected rule and the offending types; |
| 19 | +* normal `dotnet test` execution through xUnit v3. |
| 20 | + |
| 21 | +## Architecture |
| 22 | + |
| 23 | +```text |
| 24 | +Infrastructure ──► Application ──► Domain |
| 25 | +
|
| 26 | +Allowed direction: toward Domain (outer → inner) |
| 27 | +Rejected direction: Domain toward outer layers |
| 28 | +``` |
| 29 | + |
| 30 | +```text |
| 31 | +ArchitectureTests |
| 32 | + │ |
| 33 | + ├── inspects Domain |
| 34 | + ├── inspects Application |
| 35 | + └── inspects Infrastructure |
| 36 | +``` |
| 37 | + |
| 38 | +## File structure |
| 39 | + |
| 40 | +```text |
| 41 | +architecture-testing-dotnet/ |
| 42 | +├── ArchitectureGuardMinimal.slnx |
| 43 | +├── README.md |
| 44 | +├── src/ |
| 45 | +│ ├── ArchitectureGuard.Domain/ |
| 46 | +│ │ ├── ArchitectureGuard.Domain.csproj |
| 47 | +│ │ ├── DomainAssemblyMarker.cs |
| 48 | +│ │ └── Order.cs |
| 49 | +│ ├── ArchitectureGuard.Application/ |
| 50 | +│ │ ├── ArchitectureGuard.Application.csproj |
| 51 | +│ │ ├── ApplicationAssemblyMarker.cs |
| 52 | +│ │ └── GetOrderSummary.cs |
| 53 | +│ └── ArchitectureGuard.Infrastructure/ |
| 54 | +│ ├── ArchitectureGuard.Infrastructure.csproj |
| 55 | +│ ├── InfrastructureAssemblyMarker.cs |
| 56 | +│ └── InMemoryOrderRepository.cs |
| 57 | +└── tests/ |
| 58 | + └── ArchitectureGuard.ArchitectureTests/ |
| 59 | + ├── ArchitectureGuard.ArchitectureTests.csproj |
| 60 | + └── LayerRules.cs |
| 61 | +``` |
| 62 | + |
| 63 | +## Prerequisites |
| 64 | + |
| 65 | +* .NET 10 SDK |
| 66 | + |
| 67 | +No Docker, Podman, databases, API keys, or cloud credentials are required. |
| 68 | + |
| 69 | +## Run the sample |
| 70 | + |
| 71 | +Open a terminal in this sample folder and run: |
| 72 | + |
| 73 | +```powershell |
| 74 | +dotnet restore |
| 75 | +dotnet build --configuration Release --no-restore |
| 76 | +dotnet test --configuration Release --no-build |
| 77 | +``` |
| 78 | + |
| 79 | +Expected output: |
| 80 | + |
| 81 | +```text |
| 82 | +Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1 |
| 83 | +``` |
| 84 | + |
| 85 | +## Prove the rule works |
| 86 | + |
| 87 | +To verify that the architecture test genuinely enforces layer boundaries, |
| 88 | +temporarily add a forbidden dependency: |
| 89 | + |
| 90 | +1. Add an Infrastructure project reference to Domain: |
| 91 | + ```powershell |
| 92 | + dotnet add src/ArchitectureGuard.Domain/ArchitectureGuard.Domain.csproj reference src/ArchitectureGuard.Infrastructure/ArchitectureGuard.Infrastructure.csproj |
| 93 | + ``` |
| 94 | + |
| 95 | +2. Edit `src/ArchitectureGuard.Domain/Order.cs` to reference an outer type: |
| 96 | + ```csharp |
| 97 | + // Temporary violation — add this using: |
| 98 | + using ArchitectureGuard.Infrastructure; |
| 99 | + |
| 100 | + namespace ArchitectureGuard.Domain; |
| 101 | + |
| 102 | + public sealed record Order(Guid Id, decimal Total) |
| 103 | + { |
| 104 | + // Add a temporary field that references the outer layer: |
| 105 | + private static readonly InfrastructureAssemblyMarker _marker = new(); |
| 106 | + } |
| 107 | + ``` |
| 108 | + |
| 109 | +3. Run the tests again: |
| 110 | + ```powershell |
| 111 | + dotnet build --configuration Release --no-restore |
| 112 | + dotnet test --configuration Release --no-build |
| 113 | + ``` |
| 114 | + |
| 115 | + The architecture test now fails with a message like: |
| 116 | + ```text |
| 117 | + Architecture rule failed: Domain must remain independent of outer layers. |
| 118 | + - ArchitectureGuard.Domain.Order |
| 119 | + [dependency explanation] |
| 120 | + ``` |
| 121 | + |
| 122 | +4. Remove the temporary violation: |
| 123 | + ```powershell |
| 124 | + dotnet remove src/ArchitectureGuard.Domain/ArchitectureGuard.Domain.csproj reference src/ArchitectureGuard.Infrastructure/ArchitectureGuard.Infrastructure.csproj |
| 125 | + ``` |
| 126 | + Then revert `Order.cs` to its original content. |
| 127 | + |
| 128 | +5. Rebuild and confirm the test passes again. |
| 129 | + |
| 130 | +Do **not** commit the violation. |
| 131 | + |
| 132 | +## Project references |
| 133 | + |
| 134 | +```text |
| 135 | +ArchitectureGuard.Application |
| 136 | +└── references ArchitectureGuard.Domain |
| 137 | +
|
| 138 | +ArchitectureGuard.Infrastructure |
| 139 | +├── references ArchitectureGuard.Application |
| 140 | +└── references ArchitectureGuard.Domain |
| 141 | +
|
| 142 | +ArchitectureGuard.ArchitectureTests |
| 143 | +├── references ArchitectureGuard.Domain |
| 144 | +├── references ArchitectureGuard.Application |
| 145 | +└── references ArchitectureGuard.Infrastructure |
| 146 | +``` |
| 147 | + |
| 148 | +Domain references nothing. |
| 149 | + |
| 150 | +## Important boundary |
| 151 | + |
| 152 | +This repository sample intentionally does **not** reproduce the complete tutorial. |
| 153 | +The full DOTNET GUIDE tutorial additionally contains: |
| 154 | + |
| 155 | +* ArchUnitNET; |
| 156 | +* Roslyn analyzer projects; |
| 157 | +* `Microsoft.CodeAnalysis.CSharp`; |
| 158 | +* custom analyzer packaging; |
| 159 | +* analyzer unit tests; |
| 160 | +* modular-monolith slicing (Orders and Billing modules); |
| 161 | +* contracts assemblies; |
| 162 | +* reflection-based endpoint authorization checks; |
| 163 | +* naming and sealing convention suites; |
| 164 | +* value-object immutability rules; |
| 165 | +* legacy violation baselines; |
| 166 | +* multiple architecture-test frameworks; |
| 167 | +* a web API project; |
| 168 | +* controllers or Minimal API endpoints; |
| 169 | +* EF Core; |
| 170 | +* databases; |
| 171 | +* authentication; |
| 172 | +* containers; |
| 173 | +* cloud services; |
| 174 | +* production deployment; |
| 175 | +* extensive performance optimization; |
| 176 | +* complete production-readiness policy. |
| 177 | + |
| 178 | +## Verification |
| 179 | + |
| 180 | +| Item | Detail | |
| 181 | +| --- | --- | |
| 182 | +| Target framework | .NET 10 (`net10.0`) | |
| 183 | +| SDK | .NET SDK 10.0.302 | |
| 184 | +| Architecture library | `NetArchTest.eNhancedEdition` 1.4.5 | |
| 185 | +| Test framework | `xunit.v3` 3.2.2 | |
| 186 | +| Test runner | `xunit.runner.visualstudio` 3.1.5 | |
| 187 | +| Test SDK | `Microsoft.NET.Test.Sdk` 17.14.1 | |
| 188 | +| External services required | None | |
| 189 | +| Containers required | None | |
| 190 | +| API keys required | None | |
| 191 | +| Release build | Verified | |
| 192 | +| Architecture test | Verified | |
| 193 | +| Last reviewed | 2026-07-27 | |
| 194 | + |
| 195 | +## License |
| 196 | + |
| 197 | +Repository-owned sample code is available under the [MIT License](../../LICENSE). |
| 198 | + |
| 199 | +Third-party NetArchTest and xUnit packages remain subject to their own licenses. |
0 commit comments