Enforcing Software Architecture With Architecture Tests in .NET
Learn how to enforce software architecture in .NET using architecture tests with NetArchTest and ArchUnitNET, so your layers and rules stay safe over time.
The vegetable rack rule
Think about a small grocery shop near your home. The owner has a simple rule for the racks at the front. Onions go in the bottom tray, tomatoes go in the middle, and the soft fruits like bananas go on top. Why? Because if you put heavy onions on top of bananas, the bananas get squashed and nobody buys them.
For the first month the owner watches the boy who stacks the racks. But the shop gets busy. Some days the owner is at the counter and forgets to check. Slowly, onions creep to the top, bananas get crushed, and money is lost.
So the owner draws a little picture and sticks it on the wall behind the racks: onions low, tomatoes middle, fruit high. Now the rule checks itself. Anyone can glance at the wall and see if the stack is wrong. The rule no longer lives only inside the owner's tired head.
A .NET solution is just like that vegetable rack. You decide that some code sits at the bottom (the core rules of your business), some sits in the middle (the use cases), and some sits on top (the web and the database). And you decide that the heavy things must never crush the soft things. The picture on the wall is your architecture test: a small automated check that fails the build the moment someone stacks the racks wrong.
This post shows you how to write those checks in .NET, using two friendly libraries, so your architecture stays the shape you designed even years later.
What "enforcing architecture" really means
When people draw an architecture, they draw boxes and arrows. The boxes are projects or layers. The arrows say "this is allowed to use that." The whole design is really a set of rules about direction: who may depend on whom.
The trouble is that a drawing on a whiteboard cannot stop anybody. A new teammate, in a hurry, adds one line that lets the Domain layer call the database directly. The code still compiles. The tests still pass. The drawing still looks fine on the wall. But the real shape of the code has quietly drifted away from the plan.
Enforcing architecture means turning those arrows into runnable rules. A normal unit test checks behaviour: given this input, do I get the right answer? An architecture test checks shape: is the code arranged the way we agreed?
The big win is timing. A slip is caught early, while it is one line in one pull request, instead of later, when it has spread through fifty files and is painful to undo. People call this idea shifting left: moving the check as close as possible to the moment the code is written.
A picture of the layers we want to protect
Most .NET teams use some form of layered or clean architecture. The exact names vary, but the shape is usually the same. The inner core knows nothing about the outside world. The outside world is allowed to know about the core. Arrows only point inward.
Read the arrows carefully. The Domain has no arrows leaving it. It depends on nothing. That is the soft fruit at the top of the rack: precious and easy to crush. If even one arrow starts pointing out of Domain into Infrastructure, your core is no longer pure, and the whole benefit of the design leaks away.
This is exactly the kind of rule a human reviewer forgets at 6 PM on a Friday. It is also exactly the kind of rule a machine never forgets. So we hand it to a machine.
Here is what the whole loop looks like once tests are in place.
The architecture-test feedback loop
Steps
Write code
A developer adds or changes a class.
Build solution
The compiler produces DLLs.
Run arch tests
Rules inspect the compiled types.
Read result
Green means safe, red blocks the merge.
Tool one: NetArchTest
NetArchTest is a tiny library with a fluent API. You add it to a normal test project and write rules that read almost like English. It works with xUnit, NUnit, or MSTest.
First, add the package to your test project.
// In your test project's .csproj, or via the CLI:
// dotnet add package NetArchTest.Rules
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
// We point at one known type per layer to find each assembly.
private const string DomainNamespace = "MyShop.Domain";
private const string InfraNamespace = "MyShop.Infrastructure";
}Now the most important rule of all: the Domain must never depend on Infrastructure. With NetArchTest it is a single readable sentence.
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types.InAssembly(typeof(MyShop.Domain.Customer).Assembly)
.That()
.ResideInNamespace("MyShop.Domain")
.ShouldNot()
.HaveDependencyOn("MyShop.Infrastructure")
.GetResult();
Assert.True(result.IsSuccessful, BuildMessage(result));
}
// A small helper so a failure tells you exactly which types broke the rule.
private static string BuildMessage(NetArchTest.Rules.TestResult result) =>
result.IsSuccessful
? "OK"
: "These types broke the rule: " +
string.Join(", ", result.FailingTypeNames ?? new List<string>());The chain reads from left to right. Take the types in the Domain assembly, keep only those that live in the Domain namespace, and assert that they should not have a dependency on Infrastructure. If any type does, IsSuccessful is false and FailingTypeNames tells you exactly which class is the culprit. That last detail matters: a good failure message turns a five-minute hunt into a five-second fix.
You can write the same style of rule for naming. For example, every class meant to be a repository should end with the word Repository.
[Fact]
public void Repositories_Should_Have_Correct_Name_Ending()
{
var result = Types.InAssembly(typeof(MyShop.Infrastructure.CustomerRepository).Assembly)
.That()
.ImplementInterface(typeof(MyShop.Application.IRepository))
.Should()
.HaveNameEndingWith("Repository")
.GetResult();
Assert.True(result.IsSuccessful);
}One small but important note. The original NetArchTest has not had a release since 2023. A community fork called NetArchTest.eNhancedEdition keeps almost the same fluent API, fixes several bugs, and is worth using if you hit a rough edge. The code you write barely changes between them.
Tool two: ArchUnitNET
ArchUnitNET is the C# cousin of the well-known Java library ArchUnit. It is more powerful. Instead of chaining off a list of types, you first load your assemblies once, then describe full layers and the rules between them. This pays off when your rules grow beyond simple namespace checks.
// dotnet add package TngTech.ArchUnitNET.xUnit
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
public class LayerTests
{
// Load everything once, statically, so it is reused across tests.
private static readonly Architecture Architecture = new ArchLoader()
.LoadAssemblies(
typeof(MyShop.Domain.Customer).Assembly,
typeof(MyShop.Application.IRepository).Assembly,
typeof(MyShop.Infrastructure.CustomerRepository).Assembly)
.Build();
private readonly IObjectProvider<IType> DomainLayer =
Types().That().ResideInNamespace("MyShop.Domain").As("Domain");
private readonly IObjectProvider<IType> InfraLayer =
Types().That().ResideInNamespace("MyShop.Infrastructure").As("Infrastructure");
[Fact]
public void Domain_Should_Not_Use_Infrastructure()
{
IArchRule rule = Types().That().Are(DomainLayer)
.Should().NotDependOnAny(InfraLayer);
rule.Check(Architecture);
}
}Notice the shape is different. You define DomainLayer and InfraLayer as named groups, then write a rule that connects them. When the rule fails, ArchUnitNET throws with a clear message describing which class violated which layer rule. Because the architecture is loaded once into a static field, running many rules stays fast.
Choosing between the two
Both libraries do the same core job: read compiled types and check rules. The difference is how much power you need and how much ceremony you are willing to pay for it.
| Feature | NetArchTest | ArchUnitNET |
|---|---|---|
| Learning curve | Very gentle | A little steeper |
| Style | Chain off a type list | Define layers, then rules |
| Namespace rules | Yes | Yes |
| Member-level rules | Limited | Strong |
| Slice and full-layer rules | Basic | Rich |
| Active maintenance | Use the eNhancedEdition fork | Actively maintained |
| Best for | First architecture tests | Growing, demanding rule sets |
A simple way to decide: if you can describe your rule in one short English sentence about namespaces, NetArchTest is perfect and quicker to read. If your rules talk about members, attributes, inheritance trees, or many layers at once, ArchUnitNET will feel roomier.
Here is the decision as a small flow.
Picking an architecture-test library
Steps
Rule type?
Look at what you must check.
Simple?
Just namespaces and dependencies.
Complex?
Members, layers, slices, attributes.
Decide
Simple to NetArchTest, complex to ArchUnitNET.
Rules worth enforcing on day one
You do not need fifty rules. A small, well-chosen set covers most real drift. Here are the ones almost every team benefits from, with what each one protects.
| Rule | What it protects |
|---|---|
| Domain must not depend on Infrastructure | Keeps the core pure and testable |
| Application must not depend on Web | Stops use cases knowing about HTTP |
| Controllers must not use DbContext directly | Forces requests through use cases |
| Repository classes end with "Repository" | Keeps naming honest and searchable |
| Domain classes are sealed by default | Prevents surprise inheritance |
| No project references a forbidden library | Blocks banned or unlicensed packages |
That last rule is more relevant than ever. Some popular packages have changed their terms. For example, MediatR and MassTransit moved to commercial licensing for newer versions. If your team has decided to avoid a paid version, an architecture test can fail the build the moment someone adds the forbidden reference, long before it reaches production.
[Fact]
public void No_Type_Should_Reference_A_Banned_Library()
{
var result = Types.InAssembly(typeof(MyShop.Application.IRepository).Assembly)
.That()
.ResideInNamespace("MyShop.Application")
.ShouldNot()
.HaveDependencyOn("SomeBannedLibrary")
.GetResult();
Assert.True(result.IsSuccessful);
}Where these tests run
Architecture tests are just unit tests. They live in your normal test project and run with the same dotnet test command. That means your continuous integration pipeline already runs them for free. When a pull request breaks an architecture rule, the build turns red, and the merge is blocked until the shape is fixed.
This is the whole reason architecture tests are so valuable. The rule is no longer a polite request in a wiki page that nobody reads. It is a gate. The gate does not get tired, does not skip Fridays, and treats every developer exactly the same, including the senior who wrote the rule in the first place.
A gentle word of caution
Architecture tests are powerful, but they are not magic, and a few habits keep them healthy.
Keep the rules few and meaningful. If you write a rule for every tiny preference, the suite becomes noisy and people start ignoring red builds, which is worse than having no tests at all. Protect the arrows that really matter and let code review handle the rest.
Write clear failure messages. A test that only says "rule failed" wastes time. A test that says "the class OrderService in Domain depends on SqlConnectionFactory in Infrastructure" fixes the problem in seconds. Both libraries give you the failing type names, so use them.
Finally, review your rules now and then. Architecture evolves. A rule that made sense two years ago might block a sensible new pattern today. The tests should serve the design, not freeze it forever. Treat them like any other code: keep what helps, delete what no longer does.
Quick recap
- A .NET solution is like a vegetable rack: heavy code must never crush the soft, precious core. Architecture tests are the picture on the wall that keeps the stack correct.
- A normal test checks behaviour; an architecture test checks shape — which layer may depend on which.
- The most important rule is usually that the Domain depends on nothing. Arrows point inward only.
- NetArchTest gives you a tiny, English-like fluent API, perfect for first rules about namespaces and dependencies. Use the eNhancedEdition fork for active fixes.
- ArchUnitNET is more powerful for full layers, members, and slices, and is actively maintained.
- Architecture tests are just unit tests, so your CI pipeline runs them for free and blocks merges that break the rules.
- Keep rules few, meaningful, and well-messaged, and review them as the design grows. You can even block banned or newly commercial libraries like MediatR or MassTransit with a single rule.
References and further reading
- NetArchTest on GitHub (BenMorris/NetArchTest) — the fluent API library and its documentation.
- ArchUnitNET on GitHub (TNG/ArchUnitNET) — the more powerful C# port of Java's ArchUnit.
- Architecture Tests in .NET with NetArchTest.Rules — Code Maze — a clear, example-led walkthrough.
- Writing ArchUnit style tests for .NET — Ben Morris — background from the NetArchTest author.
- Why Do You Need To Write Architecture Tests in .NET — Anton Dev Tips — the case for adding them to real projects.
Related Posts
5 Architecture Tests You Should Add to Your .NET Projects
Five simple architecture tests for .NET using NetArchTest. Protect layers, naming, and dependencies with code that fails the build when rules break.
Clean Architecture Folder Structure in .NET: A Simple Guide
Learn how to organise a .NET solution with Clean Architecture. Understand the Domain, Application, Infrastructure, and Presentation layers, the dependency rule, and the exact folders, with diagrams and examples.
What Is a Modular Monolith? A Beginner-Friendly Guide for .NET
Understand the modular monolith in simple words: one app, strong internal walls. Learn how it compares to monoliths and microservices, why it is the 2026 default for most .NET teams, and how to build one.
Why Do You Need To Write Architecture Tests in .NET
Learn why architecture tests matter in .NET, how they stop your layers from drifting, and how tools like NetArchTest and ArchUnitNET keep your design safe.
Shift Left With Architecture Testing in .NET
Learn how to shift left with architecture testing in .NET using NetArchTest and ArchUnitNET, so bad dependencies are caught early before they reach production.
The Test Pyramid Is a Lie (And What I Do Instead) in .NET
The test pyramid says write mostly unit tests. In real .NET apps that often backfires. Here is the testing trophy and how I balance tests instead.