Skip to content

Commit 8771f04

Browse files
Merge pull request #2 from dotnet-guide-com/feature/architecture-testing-sample
Add lightweight architecture testing sample
2 parents e7b6e37 + 8505cdf commit 8771f04

15 files changed

Lines changed: 410 additions & 0 deletions

File tree

‎.github/workflows/build-samples.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ on:
77
- "dotnet-ai/provider-agnostic-chat-gateway/**"
88
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
99
- "dotnet-aspire/orchestrate-distributed-system/**"
10+
- "software-architecture/architecture-testing-dotnet/**"
1011
- ".github/workflows/build-samples.yml"
1112

1213
pull_request:
1314
paths:
1415
- "dotnet-ai/provider-agnostic-chat-gateway/**"
1516
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
1617
- "dotnet-aspire/orchestrate-distributed-system/**"
18+
- "software-architecture/architecture-testing-dotnet/**"
1719
- ".github/workflows/build-samples.yml"
1820

1921
workflow_dispatch:
@@ -103,3 +105,35 @@ jobs:
103105
dotnet-aspire/orchestrate-distributed-system/AspireOrchestrationMinimal.slnx \
104106
--configuration Release \
105107
--no-restore
108+
109+
test-architecture-rules:
110+
name: Test architecture rules
111+
runs-on: ubuntu-latest
112+
113+
steps:
114+
- name: Check out repository
115+
uses: actions/checkout@v5
116+
117+
- name: Install .NET 10 SDK
118+
uses: actions/setup-dotnet@v5
119+
with:
120+
dotnet-version: "10.0.x"
121+
122+
- name: Restore
123+
run: |
124+
dotnet restore \
125+
software-architecture/architecture-testing-dotnet/ArchitectureGuardMinimal.slnx
126+
127+
- name: Build
128+
run: |
129+
dotnet build \
130+
software-architecture/architecture-testing-dotnet/ArchitectureGuardMinimal.slnx \
131+
--configuration Release \
132+
--no-restore
133+
134+
- name: Test architecture rules
135+
run: |
136+
dotnet test \
137+
software-architecture/architecture-testing-dotnet/tests/ArchitectureGuard.ArchitectureTests/ArchitectureGuard.ArchitectureTests.csproj \
138+
--configuration Release \
139+
--no-build

‎README.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
1212
| [`dotnet-ai/provider-agnostic-chat-gateway`](dotnet-ai/provider-agnostic-chat-gateway/) | One HTTP chat endpoint using Ollama and optional OpenAI providers through `Microsoft.Extensions.AI.IChatClient` | [Build a switchable multi-provider AI gateway with IChatClient](https://www.dotnet-guide.com/tutorials/dotnet-ai/provider-agnostic-chat-gateway/) |
1313
| [`dotnet-ai/hybrid-search-ef-core-pgvector`](dotnet-ai/hybrid-search-ef-core-pgvector/) | Minimal Reciprocal Rank Fusion demo combining pre-ranked keyword and vector results | [Hybrid Search in .NET with EF Core 10 and pgvector](https://www.dotnet-guide.com/tutorials/dotnet-ai/hybrid-search-ef-core-pgvector/) |
1414
| [`dotnet-aspire/orchestrate-distributed-system`](dotnet-aspire/orchestrate-distributed-system/) | Minimal Aspire AppHost coordinating a web project and API with service discovery and startup ordering | [Aspire in .NET: Orchestrate, Run, and Deploy a Distributed System from One App Host](https://www.dotnet-guide.com/tutorials/dotnet-aspire/orchestrate-distributed-system/) |
15+
| [`software-architecture/architecture-testing-dotnet`](software-architecture/architecture-testing-dotnet/) | Minimal NetArchTest.eNhancedEdition rule that prevents Domain from depending on outer layers | [Architecture Testing in .NET: Enforce Layer and Module Boundaries with NetArchTest and ArchUnitNET](https://www.dotnet-guide.com/tutorials/software-architecture/architecture-testing-dotnet/) |
1516

1617
## Companion articles
1718

@@ -68,6 +69,27 @@ tutorials/
6869
| `-- AspireOrchestrationMinimal.Web/
6970
| |-- Program.cs
7071
| `-- AspireOrchestrationMinimal.Web.csproj
72+
|-- software-architecture/
73+
| `-- architecture-testing-dotnet/
74+
| |-- ArchitectureGuardMinimal.slnx
75+
| |-- README.md
76+
| |-- src/
77+
| | |-- ArchitectureGuard.Domain/
78+
| | | |-- ArchitectureGuard.Domain.csproj
79+
| | | |-- DomainAssemblyMarker.cs
80+
| | | `-- Order.cs
81+
| | |-- ArchitectureGuard.Application/
82+
| | | |-- ArchitectureGuard.Application.csproj
83+
| | | |-- ApplicationAssemblyMarker.cs
84+
| | | `-- GetOrderSummary.cs
85+
| | `-- ArchitectureGuard.Infrastructure/
86+
| | |-- ArchitectureGuard.Infrastructure.csproj
87+
| | |-- InfrastructureAssemblyMarker.cs
88+
| | `-- InMemoryOrderRepository.cs
89+
| `-- tests/
90+
| `-- ArchitectureGuard.ArchitectureTests/
91+
| |-- ArchitectureGuard.ArchitectureTests.csproj
92+
| `-- LayerRules.cs
7193
|-- .github/
7294
| `-- workflows/
7395
| `-- build-samples.yml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Solution>
2+
<Project Path="src/ArchitectureGuard.Domain/ArchitectureGuard.Domain.csproj" />
3+
<Project Path="src/ArchitectureGuard.Application/ArchitectureGuard.Application.csproj" />
4+
<Project Path="src/ArchitectureGuard.Infrastructure/ArchitectureGuard.Infrastructure.csproj" />
5+
<Project Path="tests/ArchitectureGuard.ArchitectureTests/ArchitectureGuard.ArchitectureTests.csproj" />
6+
</Solution>
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Architecture Testing in .NET &mdash; 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.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace ArchitectureGuard.Application;
2+
3+
public sealed class ApplicationAssemblyMarker;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\ArchitectureGuard.Domain\ArchitectureGuard.Domain.csproj" />
5+
</ItemGroup>
6+
7+
<PropertyGroup>
8+
<TargetFramework>net10.0</TargetFramework>
9+
<ImplicitUsings>enable</ImplicitUsings>
10+
<Nullable>enable</Nullable>
11+
</PropertyGroup>
12+
13+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace ArchitectureGuard.Application;
2+
3+
public sealed record GetOrderSummary(Guid OrderId);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace ArchitectureGuard.Domain;
2+
3+
public sealed class DomainAssemblyMarker;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace ArchitectureGuard.Domain;
2+
3+
public sealed record Order(Guid Id, decimal Total);

0 commit comments

Comments
 (0)