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.
The school bag rule
Imagine your little cousin packs her school bag every morning. There is a simple house rule: textbooks go in the big pocket, the water bottle goes in the side holder, and the lunch box goes in the front zip. Nobody minds which book she takes, but everything must sit in its proper place. If the water bottle ends up on top of the books, they get soaked.
For the first week, mum checks the bag by hand. But mum is busy. Some mornings she forgets, and one day the books really do get wet. So she sticks a small printed checklist on the door: books in big pocket, bottle in holder, lunch in front zip. Now the rule checks itself, every single day, without mum standing there.
A .NET solution is just like that school bag. You decide that the Domain code goes here, the database code goes there, and certain things must never touch each other. At first, you check these rules by hand in code reviews. But people get busy, deadlines arrive, and slowly the bottle ends up on the books. The fix is the same as mum's: write the rule down as a test that checks itself on every build.
These self-checking rules are called architecture tests. In this post you will learn five of them you can add to almost any .NET project today.
What an architecture test actually is
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?
It does this by loading your compiled assemblies and looking at the types inside: which namespace a class lives in, what it depends on, what it is named, whether it is sealed, and so on. If a class breaks a rule, the test fails, exactly like a broken unit test.
The whole point is to catch a slip early, while it is one line of code, instead of later, when it has spread through fifty files. People call this idea shifting left — moving the check as close to the moment of writing as you can.
We will use NetArchTest.Rules, the most popular and beginner-friendly library. To start, add it to your test project:
dotnet add package NetArchTest.RulesEvery rule begins from the static Types class. You load a set of types, narrow them down, ask a question, and check the result. Here is the simplest possible shape of a test:
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
// A handy reference to one type in each project,
// so we can grab its assembly later.
private const string DomainNamespace = "Shop.Domain";
private const string InfrastructureNamespace = "Shop.Infrastructure";
[Fact]
public void Domain_Should_Not_Be_Empty()
{
var result = Types.InAssembly(typeof(Shop.Domain.Order).Assembly)
.Should()
.ResideInNamespaceStartingWith(DomainNamespace)
.GetResult();
Assert.True(result.IsSuccessful, "Domain types are not where we expect.");
}
}Read it almost like English: the types in this assembly should reside in the Domain namespace. Now let us write the five rules that matter most.
A quick map of the layers we will protect
Most of these tests assume a layered solution, like Clean Architecture. The golden rule there is that dependencies point inward: the core knows nothing about the outside.
Keep this picture in mind. Three of our five tests simply turn one of these arrows into a rule that cannot be broken.
Test 1: The Domain must not depend on outer layers
This is the most important rule in a layered system. Your Domain holds the business rules — what an Order is, when it can be cancelled, how a price is calculated. It should not know that a database, a web framework, or an email service even exists. That keeps your core stable while everything around it changes.
The test reads almost like the sentence above:
[Fact]
public void Domain_Should_Not_Depend_On_OuterLayers()
{
var result = Types.InAssembly(typeof(Shop.Domain.Order).Assembly)
.ShouldNot()
.HaveDependencyOnAny(
"Shop.Application",
"Shop.Infrastructure",
"Shop.Api")
.GetResult();
Assert.True(
result.IsSuccessful,
BuildMessage("Domain must not depend on outer layers", result));
}That helper BuildMessage turns the failing types into a friendly error so the developer knows exactly what broke:
private static string BuildMessage(string rule, NetArchTest.Rules.TestResult result)
{
if (result.IsSuccessful) return rule;
var names = string.Join(", ", result.FailingTypeNames);
return $"{rule}. Offending types: {names}";
}If someone adds a sneaky using Shop.Infrastructure; inside a Domain class to "just quickly save something," this test goes red on the very next build. The water bottle never lands on the books.
How Test 1 protects the core
Steps
Bad using added
Someone references Infrastructure inside Domain
Test scans Domain
NetArchTest loads every Domain type
Finds outer reference
A dependency points outward — not allowed
Build fails fast
Red test before the code is merged
Test 2: The Application layer must not touch Infrastructure directly
The Application layer holds your use cases — "place an order", "register a user." It is allowed to depend on the Domain. But it should talk to the outside world only through interfaces, not by reaching directly into Infrastructure classes.
Why? Because if your use cases call EF Core or SmtpClient directly, you can no longer swap the database or test the use case without a real one. The interface is the thin curtain between "what I need" and "how it is done."
[Fact]
public void Application_Should_Not_Depend_On_Infrastructure()
{
var result = Types.InAssembly(typeof(Shop.Application.PlaceOrder).Assembly)
.ShouldNot()
.HaveDependencyOn("Shop.Infrastructure")
.GetResult();
Assert.True(
result.IsSuccessful,
BuildMessage("Application must not depend on Infrastructure", result));
}Here is the contrast in a small table.
| Without the test | With the test |
|---|---|
Use case calls DbContext directly | Use case calls an IOrderRepository interface |
| Hard to unit test (needs a real DB) | Easy to test with a fake repository |
| Database choice leaks into core code | Database choice stays in Infrastructure |
| A wrong dependency hides for months | Build fails the same hour it is added |
The Domain or Application defines the interface; Infrastructure implements it. This idea is the Dependency Inversion Principle, and this single test keeps it honest.
Test 3: Naming conventions stay consistent
Names are a quiet form of documentation. When every repository ends in Repository and every command handler ends in Handler, a new teammate can guess what a file does just from its name. But naming slips easily — one person writes OrderRepo, another writes OrdersStore, and the pattern dissolves.
A naming test locks the pattern in place. Here we say: every class that implements IRepository must end with the word Repository.
[Fact]
public void Repositories_Should_Have_Repository_Suffix()
{
var result = Types.InAssembly(typeof(Shop.Infrastructure.OrderRepository).Assembly)
.That()
.ImplementInterface(typeof(Shop.Application.IRepository))
.Should()
.HaveNameEndingWith("Repository")
.GetResult();
Assert.True(
result.IsSuccessful,
BuildMessage("Repositories must end with 'Repository'", result));
}You can write the same kind of rule for many patterns. Here are common ones teams enforce.
| Pattern | Rule | Example name |
|---|---|---|
| Repository | Ends with Repository | OrderRepository |
| Command handler | Ends with Handler | PlaceOrderHandler |
| API controller | Ends with Controller | OrdersController |
| Domain event | Ends with Event | OrderPlacedEvent |
| Options class | Ends with Options | EmailOptions |
Consistent names will not make your code run faster, but they make it far easier to read, search, and trust — and that saves real time on every future change.
Test 4: No layer-skipping (controllers must not call repositories)
In a clean flow, a request travels through the layers in order: the controller asks the Application to do work, and only the Application reaches the repository. A controller that calls a repository directly is "skipping the queue." It might feel quick, but it scatters business logic into the web layer where it does not belong.
The test says: presentation types must not depend on repository types.
[Fact]
public void Controllers_Should_Not_Depend_On_Repositories()
{
var result = Types.InAssembly(typeof(Shop.Api.OrdersController).Assembly)
.That()
.HaveNameEndingWith("Controller")
.ShouldNot()
.HaveDependencyOn("Shop.Infrastructure.Repositories")
.GetResult();
Assert.True(
result.IsSuccessful,
BuildMessage("Controllers must not call repositories directly", result));
}This keeps the controller thin: receive the request, hand it to the Application, return the result. All the real thinking stays in one place, which makes the code easier to test and to change.
Stopping a layer-skip
Steps
Shortcut written
Controller injects a repository directly
Test checks controllers
Rule scans every *Controller type
Bad path found
Direct repository dependency detected
Refactor through handler
Call goes via the Application use case
Test 5: Handlers should be sealed (and other "shape" rules)
The last test is a small but powerful kind: enforcing a structural shape rather than a dependency. A common example is sealing your handler classes.
Marking a class sealed says "nobody should inherit from this." For things like command handlers, that is usually exactly what you want — they are end-of-the-line classes, not base classes. Sealing also gives the runtime a tiny performance hint and stops surprising inheritance bugs.
[Fact]
public void Handlers_Should_Be_Sealed()
{
var result = Types.InAssembly(typeof(Shop.Application.PlaceOrderHandler).Assembly)
.That()
.HaveNameEndingWith("Handler")
.Should()
.BeSealed()
.GetResult();
Assert.True(
result.IsSuccessful,
BuildMessage("All handlers must be sealed", result));
}NetArchTest can check many such shapes. A few you might find useful:
BeSealed()— the class cannot be inherited.BePublic()/NotBePublic()— controls who can see the type.BeImmutable()— useful for value objects and records.HaveCustomAttribute(...)— every handler must carry a certain attribute.OnlyHaveDependenciesOn(...)— a strict allow-list of references.
Pick the ones that match the rules your team already cares about. A good architecture test simply writes down a decision you have already made in your head.
A small note on libraries and licences
When you set up these tests, you will often see MediatR mentioned for command handlers. It is worth knowing that MediatR and MassTransit moved to a commercial licence for newer versions. They are still fine to use, but check the licence and pricing before you adopt them in a paid product. Architecture tests themselves do not need either library — you can hand-write tiny handler interfaces and keep everything free and simple. NetArchTest is open source and has no such cost.
This is also a good moment to mention versions: as of today, .NET 10 is the LTS release, C# 14 has shipped, and C# 15 union types are arriving in the .NET 11 previews. None of these change how architecture tests work — the Types API stays the same — but it is nice to know you are building on current ground.
Putting it all together in CI
A test that nobody runs protects nothing. The real magic happens when these five tests run automatically on every push. Then a broken rule cannot be merged at all.
Because these tests have no database and no web server, they run in a flash. You add them once, and they guard your design forever — like mum's printed checklist on the door, doing its quiet job every single morning.
A simple plan to adopt them:
- Create one test project, for example
Shop.ArchitectureTests. - Add the
NetArchTest.Rulespackage. - Copy the five tests above and change the namespaces to match your solution.
- Run them locally until they are green.
- Make sure your CI runs
dotnet testso they cannot be skipped.
Quick recap
- Architecture tests check the shape of your code, not its behaviour — the way mum's checklist guards the school bag.
- Test 1: the Domain must not depend on outer layers, keeping your business rules stable and pure.
- Test 2: the Application must talk to the outside only through interfaces, never Infrastructure directly.
- Test 3: naming conventions (like the
Repositorysuffix) stay consistent so code is easy to read and search. - Test 4: no layer-skipping — controllers go through the Application, never straight to a repository.
- Test 5: structural rules like sealed handlers lock in decisions you already made.
- Run them in CI so a broken rule fails the build the same hour it is written. They are fast, free with NetArchTest, and pay you back on every future change.
References and further reading
- NetArchTest on GitHub (BenMorris/NetArchTest) — the library, its fluent API, and examples.
- Architecture Tests in .NET with NetArchTest.Rules — Code Maze — a friendly walkthrough with sample rules.
- Shift Left With Architecture Testing in .NET — Milan Jovanović — why catching issues early matters.
- ArchUnitNET on GitHub (TNG/ArchUnitNET) — a more powerful alternative when you outgrow simple rules.
Related Posts
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.
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.
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.
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.
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.