Skip to main content
SEMastery
Testingintermediate

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.

12 min readUpdated May 25, 2026

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?

Figure 1: An architecture test reads your compiled types, checks them against a rule, then passes or fails like any other test.

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.

Figure 2: Allowed dependency directions in a clean architecture. Arrows point inward only.

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

Write code
Build solution
Run arch tests
Read result

Steps

1

Write code

A developer adds or changes a class.

2

Build solution

The compiler produces DLLs.

3

Run arch tests

Rules inspect the compiled types.

4

Read result

Green means safe, red blocks the merge.

From writing code to a clear pass or fail signal.

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.

FeatureNetArchTestArchUnitNET
Learning curveVery gentleA little steeper
StyleChain off a type listDefine layers, then rules
Namespace rulesYesYes
Member-level rulesLimitedStrong
Slice and full-layer rulesBasicRich
Active maintenanceUse the eNhancedEdition forkActively maintained
Best forFirst architecture testsGrowing, 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

Rule type?
Simple?
Complex?
Decide

Steps

1

Rule type?

Look at what you must check.

2

Simple?

Just namespaces and dependencies.

3

Complex?

Members, layers, slices, attributes.

4

Decide

Simple to NetArchTest, complex to ArchUnitNET.

A quick path from your needs to a sensible default.

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.

RuleWhat it protects
Domain must not depend on InfrastructureKeeps the core pure and testable
Application must not depend on WebStops use cases knowing about HTTP
Controllers must not use DbContext directlyForces requests through use cases
Repository classes end with "Repository"Keeps naming honest and searchable
Domain classes are sealed by defaultPrevents surprise inheritance
No project references a forbidden libraryBlocks 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.

Figure 3: A pull request flows through the pipeline; broken architecture rules block the merge.

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

Related Posts