.NET architecture tests - fitness functions
dotnet-architecture decides what the structure should be (clean, vertical-slice, DDD, modular, microservices); this skill makes a test prove it, and fail the build the moment a boundary is crossed. Without that, a layering rule lives only in a diagram and a reviewer's memory, so it erodes silently - one stray using at a time - until the next big refactor. This is the enforcement counterpart to those concept skills, exactly as dotnet-code-quality enforces the csharp style rules. Baseline is .NET 8 / C# 12.
Pick the library: NetArchTest by default
Default to NetArchTest.Rules: lightweight, fluent, fast, and zero-ceremony - assertions over namespaces, dependencies, and layering that read cleanly in a normal test.
[Fact]
public void Domain_depends_on_nothing_outside_itself()
{
var result = Types.InAssembly(typeof(Order).Assembly)
.That().ResideInNamespace("Shop.Domain")
.ShouldNot().HaveDependencyOnAny("Microsoft.EntityFrameworkCore", "Shop.Infrastructure", "Microsoft.AspNetCore")
.GetResult();
result.IsSuccessful.Should().BeTrue(
because: $"domain must stay pure; offenders: {string.Join(", ", result.FailingTypeNames)}");
}
Reach for ArchUnitNET only when NetArchTest's model is too thin - it offers richer modeling (slice analysis, namespace/assembly cycle detection, custom conditions and predicates) at the cost of a heavier API. Pick one per solution; do not run both.
The rules worth enforcing
In priority order - start at the top, add lower rows as conventions actually stabilize:
- Dependency direction (the highest-value rule). Domain depends on nothing; Application depends on abstractions, not Infrastructure / EF / ASP.NET; the web layer talks to Application, never reaching past it into
DbContext directly (no layer-skipping). In a layered project these are the layers; in a vertical-slice project they are the parts of a feature - the rule is the same.
- Slice / module isolation. A feature namespace must not reference another feature's internals - the property that keeps vertical slices independent.
- Naming, sealing, placement. Conventions the team relies on: handlers end in
Handler and are sealed, abstractions live in the abstractions namespace, nothing is public that was meant to be internal.
- No cycles between namespaces or modules.
- No leftover debug output. Fail the build when a production assembly depends on
Console.WriteLine, Debug.WriteLine, or Debugger.Break - a ShouldNot().HaveDependencyOn over those types, so a stray trace statement left in mid-debug can never ship.
Wire it as a real test
- A dedicated test project (e.g.
Architecture.Tests) referencing the assemblies under test, one test per rule, each named for the rule it guards. It runs inside the normal dotnet test / CI pass - it is not a separate manual gate that gets skipped under pressure.
- Resolve assemblies by a type marker (
typeof(Order).Assembly), never by loading an assembly by string name - a rename silently turns a string-named rule into a no-op that passes forever.
- Make the failure actionable. Assert on the result and surface the offending type names in the message, so a red test names the class that broke the rule instead of just saying
false.
Keep them honest
- One rule per test, like any test, so a failure pinpoints the exact violation.
- These are structural rules - they complement, never replace, the behavioral tests in
dotnet-testing.
- Encode only rules the team has actually agreed to. A wall of brittle naming rules nobody signed up for becomes noise people disable - at which point the suite enforces nothing.
Anti-patterns
- Architecture that lives only in prose and diagrams, enforced by no one - the exact gap this skill closes.
- Resolving assemblies by string name instead of a type marker, so a rename quietly disables the rule.
- A rule test that computes a result but never asserts on it (or discards it) - it passes vacuously; this is the false-confidence smell
dotnet-testing's audit hunts for.
- Over-specifying: dozens of fragile naming rules that outpace the team's real conventions, so the architecture suite gets switched off.
- Hiding the rules in a separate manual step instead of the normal test run, so they are skipped exactly when a deadline makes boundaries most likely to slip.
1---2name: dotnet-architecture-tests3description: .NET conventions for architecture fitness tests - encoding layer/dependency/naming/isolation rules as tests that fail the build on a violation, so the structure dotnet-architecture prescribes cannot erode silently. Floors at .NET 8 / C# 12. Load when adding or reviewing architecture / fitness tests or enforcing layer/dependency rules, or when the user names NetArchTest, ArchUnitNET, fitness function, architecture test, or dependency rule. Companions: dotnet-testing (the test-suite host), dotnet-architecture (the boundaries enforced), dotnet-code-quality (the analyzer/style counterpart). Do NOT load for runtime behavior tests (dotnet-testing) or analyzer/formatter config (dotnet-code-quality).4---56# .NET architecture tests - fitness functions78`dotnet-architecture` decides *what* the structure should be (clean, vertical-slice, DDD, modular, microservices); this skill makes a test *prove* it, and fail the build the moment a boundary is crossed. Without that, a layering rule lives only in a diagram and a reviewer's memory, so it erodes silently - one stray `using` at a time - until the next big refactor. This is the enforcement counterpart to those concept skills, exactly as `dotnet-code-quality` enforces the `csharp` style rules. Baseline is .NET 8 / C# 12.910## Pick the library: NetArchTest by default1112Default to **NetArchTest.Rules**: lightweight, fluent, fast, and zero-ceremony - assertions over namespaces, dependencies, and layering that read cleanly in a normal test.1314```csharp15[Fact]16public void Domain_depends_on_nothing_outside_itself()17{18 var result = Types.InAssembly(typeof(Order).Assembly)19 .That().ResideInNamespace("Shop.Domain")20 .ShouldNot().HaveDependencyOnAny("Microsoft.EntityFrameworkCore", "Shop.Infrastructure", "Microsoft.AspNetCore")21 .GetResult();2223 result.IsSuccessful.Should().BeTrue(24 because: $"domain must stay pure; offenders: {string.Join(", ", result.FailingTypeNames)}");25}26```2728Reach for **ArchUnitNET** only when NetArchTest's model is too thin - it offers richer modeling (slice analysis, namespace/assembly cycle detection, custom conditions and predicates) at the cost of a heavier API. Pick one per solution; do not run both.2930## The rules worth enforcing3132In priority order - start at the top, add lower rows as conventions actually stabilize:3334- **Dependency direction** (the highest-value rule). Domain depends on nothing; Application depends on abstractions, not Infrastructure / EF / ASP.NET; the web layer talks to Application, never reaching past it into `DbContext` directly (no layer-skipping). In a layered project these are the layers; in a vertical-slice project they are the parts of a feature - the rule is the same.35- **Slice / module isolation**. A feature namespace must not reference another feature's internals - the property that keeps vertical slices independent.36- **Naming, sealing, placement**. Conventions the team relies on: handlers end in `Handler` and are `sealed`, abstractions live in the abstractions namespace, nothing is `public` that was meant to be `internal`.37- **No cycles** between namespaces or modules.38- **No leftover debug output**. Fail the build when a production assembly depends on `Console.WriteLine`, `Debug.WriteLine`, or `Debugger.Break` - a `ShouldNot().HaveDependencyOn` over those types, so a stray trace statement left in mid-debug can never ship.3940## Wire it as a real test4142- A **dedicated test project** (e.g. `Architecture.Tests`) referencing the assemblies under test, one test per rule, each named for the rule it guards. It runs inside the normal `dotnet test` / CI pass - it is not a separate manual gate that gets skipped under pressure.43- **Resolve assemblies by a type marker** (`typeof(Order).Assembly`), never by loading an assembly by string name - a rename silently turns a string-named rule into a no-op that passes forever.44- **Make the failure actionable.** Assert on the result *and* surface the offending type names in the message, so a red test names the class that broke the rule instead of just saying `false`.4546## Keep them honest4748- One rule per test, like any test, so a failure pinpoints the exact violation.49- These are *structural* rules - they complement, never replace, the behavioral tests in `dotnet-testing`.50- Encode only rules the team has actually agreed to. A wall of brittle naming rules nobody signed up for becomes noise people disable - at which point the suite enforces nothing.5152## Anti-patterns5354- Architecture that lives only in prose and diagrams, enforced by no one - the exact gap this skill closes.55- Resolving assemblies by string name instead of a type marker, so a rename quietly disables the rule.56- A rule test that computes a result but never asserts on it (or discards it) - it passes vacuously; this is the false-confidence smell `dotnet-testing`'s audit hunts for.57- Over-specifying: dozens of fragile naming rules that outpace the team's real conventions, so the architecture suite gets switched off.58- Hiding the rules in a separate manual step instead of the normal test run, so they are skipped exactly when a deadline makes boundaries most likely to slip.