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 leaky pipe you find too late
Imagine a new house is being built in your colony. The plumber lays all the pipes inside the walls. Then the painters come, the walls are plastered, and beautiful tiles go up. Everything looks lovely.
Three months later, a small leak starts behind the kitchen wall. Now fixing it is a nightmare. You have to break the tiles, chip the plaster, repair the pipe, and redo everything. The same leak, if it had been spotted on the very first day while the pipes were still open, would have taken five minutes and almost no money.
Software is exactly like this. A small mistake in how your code is wired together is cheap to fix on day one. The same mistake, found after months of building on top of it, is painful and expensive.
"Shift left" is a simple idea that fixes this. It means moving your checks to an earlier moment, so you find the leaky pipe while the wall is still open. In .NET, architecture testing is one of the best ways to shift left for the structure of your code.
What does "left" even mean?
Think of your work as a line. On the far left you are typing code. A little to the right you push it. Further right it gets reviewed and merged. Even further right it gets deployed. On the far right, a real customer is using it.
The cost of a bug grows fast as it travels right.
The cost of finding a problem grows as it moves right
Steps
Coding
Cheapest. A few seconds to fix.
Push
Still cheap. CI shows red.
Review
Costs a teammate's time.
Deploy
Now a rollback may be needed.
Customer
Most expensive. Trust is lost.
Shifting left means we want the red light to appear as far to the left as possible. The architecture test is our red light for code structure.
What is an architecture test?
A normal unit test checks what your code does. For example, "when I add 2 and 2, do I get 4?"
An architecture test checks the shape of your code. For example, "is my Domain project secretly talking to the database?" It does not care about behaviour. It cares about the rules that keep your codebase tidy.
Here are some rules an architecture test can guard:
| Rule | Plain meaning |
|---|---|
| Domain must not depend on Infrastructure | Your core logic stays clean and free of database details. |
| Controllers must not call the database directly | Web code goes through a service, not straight to data. |
Classes ending in Service live in the Application layer | Naming and folders stay consistent. |
| No project depends on the UI project | The UI is the top, nothing should rely on it. |
The best part is that these tests run with your normal test runner, like dotnet test. So they break the build the same way a failing unit test would. That is what makes them a perfect shift-left tool.
A common project shape
Most clean .NET solutions are split into layers. Each layer is allowed to depend only on the layers below it. The arrows below show the allowed direction of dependencies.
The most important rule here is that Domain depends on nobody. It is the heart of your app. If Domain starts depending on Infrastructure, your business rules get tangled up with database code, and that is the leaky pipe we want to catch early.
Choosing a library
Two friendly libraries help you write these tests in .NET. Here is a quick comparison so you can pick.
| Library | Best for | Notes |
|---|---|---|
| NetArchTest | Small, readable rules about namespaces and dependencies | Tiny fluent API. The original repo is quiet since 2023, so many use the NetArchTest.eNhancedEdition fork. |
| ArchUnitNET | Richer rules, full layer definitions, member checks | C# port of the popular Java ArchUnit. More power, slightly more setup. |
A good plan for a beginner is to start with NetArchTest. When your rules grow more demanding, move to ArchUnitNET. Both are free and open source.
Your first architecture test with NetArchTest
Let us write a test that guards our most important rule: the Domain layer must never depend on Infrastructure.
First, add the package to your test project.
dotnet add package NetArchTest.RulesNow write the test. We grab a type from the Domain assembly so the library knows where to look, then we state the rule in plain, fluent English.
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
// Pick any type that lives in the Domain project.
var domainAssembly = typeof(Order).Assembly;
var result = Types
.InAssembly(domainAssembly)
.Should()
.NotHaveDependencyOn("MyShop.Infrastructure")
.GetResult();
Assert.True(result.IsSuccessful, "Domain must not reference Infrastructure.");
}
}Read it like a sentence. "Types in the Domain assembly should not have a dependency on MyShop.Infrastructure." If someone adds such a reference, result.IsSuccessful becomes false, and the test fails on the very next run. That is shifting left in action.
Guarding naming and folders too
Architecture tests are not only about dependencies. They can keep your naming tidy too. Say your team agreed that every service class should end with the word Service. You can write that as a rule.
[Fact]
public void Service_Classes_Should_End_With_Service()
{
var result = Types
.InAssembly(typeof(OrderService).Assembly)
.That()
.ResideInNamespace("MyShop.Application.Services")
.Should()
.HaveNameEndingWith("Service")
.GetResult();
Assert.True(result.IsSuccessful);
}When the rule fails, NetArchTest can even hand you the list of offending types through result.FailingTypeNames, so you know exactly which class to rename. No guessing.
The same idea with ArchUnitNET
ArchUnitNET feels similar but lets you describe full layers and reuse them. You load your assemblies once, then write rules against named layers. This shines when your rules get bigger.
using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using ArchUnitNET.Fluent;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
public class LayerTests
{
private static readonly Architecture Architecture =
new ArchLoader()
.LoadAssemblies(
typeof(Order).Assembly,
typeof(OrderService).Assembly)
.Build();
[Fact]
public void Domain_Should_Not_Depend_On_Application()
{
IArchRule rule = Types()
.That().ResideInNamespace("MyShop.Domain")
.Should().NotDependOnAny(
Types().That().ResideInNamespace("MyShop.Application"));
rule.Check(Architecture);
}
}Notice the shape is the same: name the types, state what they should not do, and check. If the rule breaks, rule.Check throws and your test goes red.
How it fits into your day
Here is the loop you want. The architecture test sits right next to you while you code, then again in CI as a backup.
The shift-left feedback loop
Steps
Edit code
You add a new reference.
Run tests
dotnet test runs in seconds.
Red or green
Architecture test tells you instantly.
Fix or push
Fix locally if red, else push.
CI re-checks
Pipeline blocks bad merges.
The diagram below shows the same flow as a decision. The key point is that the bad code never gets the chance to travel far right.
Why this saves real time
Let us make the saving concrete. Suppose a teammate, in a hurry, makes the Domain project reference Infrastructure so they can quickly call a database helper. Without architecture tests, that small shortcut gets merged. Over the next few weeks, ten more classes lean on it. Now untangling it is a big, risky job.
With architecture tests, the moment they run dotnet test, the build goes red and says, in plain words, that Domain must not reference Infrastructure. They fix it in two minutes by moving the helper. The leaky pipe never made it behind the wall.
This is the heart of shift left. You are not adding more work. You are moving the same check to an earlier, cheaper moment.
A few starter rules worth adding
If you are starting today, these four rules give a lot of safety for very little effort. Add them one at a time.
- Domain must not depend on any other project. Protects your core.
- The API or Web project must not be depended on by anyone. The top should stay at the top.
- Classes ending in
Repositorymust live in the Infrastructure layer. Keeps data code in one place. - Controllers must not reference the database context directly. Forces them to go through a service.
Each of these is just a handful of lines, and each one closes a door that messy code likes to sneak through.
Common beginner mistakes
A few small traps catch people early. Keep these in mind.
| Mistake | What happens | Fix |
|---|---|---|
| Wrong namespace string | The rule matches nothing and always passes | Double-check spelling, like MyShop.Domain |
| Testing source files | You try to read .cs files | These tools read compiled assemblies, not source |
| Forgetting CI | Rules only run on one laptop | Add dotnet test to your pipeline too |
The middle one is worth repeating. Architecture tests look at your built code, the DLL files, not your text files. That is why they are so fast and why your test project must reference the projects it checks.
A note on the wider ecosystem
You may have heard that some popular libraries changed their licensing. For example, MediatR and MassTransit have moved to a commercial license for newer versions. This does not affect NetArchTest or ArchUnitNET, which remain free and open source. But it is a good reminder that architecture tests can also guard against accidentally using a library you are not allowed to. You could write a rule that says, for instance, no project may depend on a banned namespace. The tools are flexible enough for that.
Writing a rule the whole team can read
One quiet benefit of architecture tests is that they double as documentation. A new joiner can open the test file and read, in plain sentences, what the rules of the codebase are. There is no need to dig through a long wiki page that nobody updated. The rule and the check are the same thing, so they can never drift apart.
Try to give each test a clear name and a clear message. Compare these two. The first one fails with a confusing red line. The second one tells the reader exactly what went wrong and why it matters.
// Hard to understand when it fails.
Assert.True(result.IsSuccessful);
// Kind to the next person who sees the red light.
Assert.True(
result.IsSuccessful,
"Domain must not reference Infrastructure. " +
"Move the database helper into the Infrastructure project instead.");A small message like this turns a scary failing build into a friendly note. The person who broke the rule learns the reason and the fix in one line. That is shift left for knowledge too, not just for bugs. The earlier a teammate understands a rule, the less likely they are to break it again.
References and further reading
- Shift Left With Architecture Testing in .NET — Milan Jovanovic
- NetArchTest on GitHub (BenMorris/NetArchTest)
- ArchUnitNET on GitHub (TNG/ArchUnitNET)
- Architecture Tests in .NET with NetArchTest.Rules — Code Maze
- NetArchTest.Rules on NuGet
Quick recap
- Shift left means catching problems early, while the fix is still cheap, just like spotting a leaky pipe before the wall is sealed.
- The further right a bug travels, from coding to a real customer, the more it costs to fix.
- Architecture tests check the shape of your code, not its behaviour, like which project may depend on which.
- They run with your normal test runner, so they go red the same way a unit test does. That is what makes them a great shift-left tool.
- NetArchTest is a tiny, friendly fluent API. ArchUnitNET is more powerful for bigger rule sets. Both are free.
- The most important rule for most apps is that Domain depends on nobody.
- Run the tests both locally for instant feedback and in CI so bad merges are blocked.
- These tools read your compiled DLL files, which is why they finish in well under a second.
Related Posts
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.
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.
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.
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.
Synchronous vs Asynchronous Communication in Microservices (.NET Guide)
A simple, friendly guide to synchronous vs asynchronous communication in microservices, with .NET examples, diagrams, tables, and clear rules on when to use each.