# Architecture Fitness Functions in .NET With ArchUnitNET

> An architecture rule nobody enforces is a wish with a code-review lottery attached. A fitness function turns it into a build failure that names the offending type. Here is how to write them with ArchUnitNET, including slice cycle detection, the three ways a fitness function quietly lies to you, and what to do when your rule set outgrows code.

Published: 2026-07-25. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/architecture-fitness-functions-archunitnet

An **architecture fitness function** is an automated, executable check of a structural property of your system: dependency direction, naming conventions, cycle freedom.
Where a unit test asserts behavior, a fitness function asserts shape, and running one in CI turns an architecture rule from documentation into a build failure that names the offending type.
Here's how to write them in .NET with ArchUnitNET.

Every team has architecture rules.
"Application must not reference Infrastructure."
"Handlers are sealed and end in `Handler`."
"Modules talk through contracts, never through each other's internals."

Almost no team has them **enforced**, which means they are not rules.
They are wishes with a code-review lottery attached, and they lose that lottery on the busy weeks, which are the weeks that matter.

A fitness function is the fix, and it is less work than the rule was to agree on.

## What Is an Architecture Fitness Function?

The term comes from evolutionary architecture: an automated check of a **structural** property of your system, run like any other test.

A unit test asserts behavior.
Given this input, the method returns that output.

A fitness function asserts **shape**.
Nothing in the application layer references the infrastructure layer.
No two feature folders depend on each other in a loop.
Only the persistence assembly knows that EF Core exists.

That distinction matters because shape is what erodes.
Behavior breaks loudly and immediately.
A single new `using` in the wrong file breaks nothing today, ships fine, and shows up two years later as the reason nobody can extract that module.

I have written before about [**enforcing architecture with tests**](https://milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests), and the general case for [**shifting that feedback left**](https://milanjovanovic.tech/blog/shift-left-with-architecture-testing-in-dotnet) holds regardless of tool.
This article is about the heavier of the two .NET libraries, [**ArchUnitNET**](https://github.com/TNG/ArchUnitNET), a port of Java's ArchUnit, and what it can express that lighter tools cannot.

## Setting It Up

Two packages: the core library and the adapter for your test framework.

```bash
dotnet add package TngTech.ArchUnitNET
dotnet add package TngTech.ArchUnitNET.xUnitV3
```

Loading the architecture is the expensive step, because ArchUnitNET parses the IL of every assembly you hand it.
Do it once for the whole test run:

```csharp
using ArchUnitNET.Domain;
using ArchUnitNET.Loader;

public static class ArchitectureFixture
{
    // Loaded once, shared by every rule. Parsing the assemblies is the slow part;
    // evaluating a rule against an already-built Architecture is in-memory work.
    public static readonly Architecture Architecture = new ArchLoader()
        .LoadAssemblies(
            typeof(Shop.Domain.AssemblyReference).Assembly,
            typeof(Shop.Application.AssemblyReference).Assembly,
            typeof(Shop.Infrastructure.AssemblyReference).Assembly,
            typeof(Shop.Api.AssemblyReference).Assembly)
        .Build();
}
```

The `AssemblyReference` marker is just an empty public class in each project's root, so the test project can name an assembly without hardcoding its string name.

Now the first rule:

```csharp
using ArchUnitNET.Fluent;
using ArchUnitNET.xUnitV3;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

public class LayeringTests
{
    [Fact]
    public void Application_Should_Not_Depend_On_Infrastructure()
    {
        IArchRule rule = Types()
            .That().ResideInNamespace("Shop.Application", useRegularExpressions: false)
            .Should().NotDependOnAny(
                Types().That().ResideInNamespace("Shop.Infrastructure", useRegularExpressions: false));

        rule.Check(ArchitectureFixture.Architecture);
    }
}
```

`Check` comes from the test-framework adapter package.
It throws when the rule fails, and the message names every offending type and the dependency that violated the rule, which is the entire reason to use a library instead of reflection by hand.

## The Rules Worth Writing First

Four shapes cover most of what teams actually argue about in reviews.

**Dependency direction.** The rule above, once per boundary you care about. In [**Clean Architecture**](https://milanjovanovic.tech/blog/clean-architecture-dotnet) that is domain depending on nothing, application depending on domain only. In a [**modular monolith**](https://milanjovanovic.tech/blog/modular-monolith-architecture-dotnet) it is one rule per module pair, allowing the contracts namespace and forbidding everything else.

**Library containment.** Keep infrastructure concerns from leaking inward:

```csharp
IArchRule domainIsPersistenceIgnorant = Types()
    .That().ResideInNamespace("Shop.Domain", useRegularExpressions: false)
    .Should().NotDependOnAny(
        Types().That().ResideInNamespace("Microsoft.EntityFrameworkCore", useRegularExpressions: false));
```

This one earns its keep faster than the layering rules, because an EF Core attribute on a domain entity looks harmless in a diff.

**Conventions.** The things you keep retyping in review comments:

```csharp
IArchRule handlerConventions = Classes()
    .That().ImplementInterface("IRequestHandler")
    .Should().BeSealed()
    .AndShould().HaveNameEndingWith("Handler");
```

**Cycle freedom.** Covered next, because it is the one you should not try to write yourself.

I listed my own starting set in [**5 architecture tests every .NET project should have**](https://milanjovanovic.tech/blog/5-architecture-tests-you-should-add-to-your-dotnet-projects).

## Cycle Freedom Is the Rule You Cannot Hand-Roll

ArchUnitNET can slice a codebase by a namespace pattern and assert that the slices form no dependency cycles:

```csharp
using static ArchUnitNET.Fluent.Slices.SliceRuleDefinition;

IArchRule noSliceCycles = Slices()
    .Matching("Shop.Features.(*)")
    .Should().BeFreeOfCycles();
```

`Matching` captures one slice per distinct value of `(*)`, so `Shop.Features.Orders`, `Shop.Features.Billing`, and `Shop.Features.Shipping` each become a node, and the rule fails if the dependency graph between them contains a loop.

This is the rot that turns a [**vertical slice architecture**](https://milanjovanovic.tech/blog/vertical-slice-architecture-dotnet) into mud, and it is close to invisible in review.
Orders reaches into Billing for a tax calculation.
Months later Billing reaches into Orders for a customer lookup.
Neither pull request looked wrong on its own, and now the two features cannot be understood, tested, or extracted separately.

Detecting that means building a dependency graph and running cycle detection over it.
You can write that, but it is a genuine algorithm with genuine edge cases, and it is the main reason I reach for ArchUnitNET over lighter alternatives like [**NetArchTest**](https://github.com/BenMorris/NetArchTest), which has no equivalent.

## When You Need the Verdict, Not the Exception

`Check` is right inside a test: it throws, the runner prints the message, done.

When the result feeds something other than a test runner (a report, a dashboard, a per-rule verdict on a build page), evaluate the rule and read the results yourself:

```csharp
foreach (EvaluationResult result in noSliceCycles.Evaluate(ArchitectureFixture.Architecture))
{
    if (!result.Passed)
    {
        Console.WriteLine(result.Description); // names the object and why it failed
    }
}
```

Each `EvaluationResult` carries `Passed`, a `Description`, and the object that was evaluated, so you can group violations by rule, count them over time, or render them next to the code that caused them.

## Three Ways a Fitness Function Lies to You

All three produce a green build for a codebase that is violating the rule.
They are worth knowing before you start trusting the suite.

**An empty rule set always passes.**
`Classes().That().ImplementInterface("IRequestHandler")` matching zero types means "every one of the zero handlers is sealed", which is true.
Rename the interface, and the convention rule goes green forever without a single handler being checked.
Guard the rules that matter:

```csharp
[Fact]
public void Handler_Convention_Rule_Has_Something_To_Check()
{
    IEnumerable<Class> handlers = Classes()
        .That().ImplementInterface("IRequestHandler")
        .GetObjects(ArchitectureFixture.Architecture);

    Assert.NotEmpty(handlers);
}
```

**Namespace strings do not get refactored.**
Rename `Shop.Persistence` to `Shop.Infrastructure.Persistence` and every rule written against the old string silently matches nothing.
Prefer assembly-based selection where you can, and when you do use namespaces, keep them in one `const` per layer so a rename is one edit instead of a search.

**A rule can encode an accident.**
Write the rules by reading the current code and you will faithfully enshrine whatever shortcut is in there today.
Write them from the decision you actually made, watch them fail, then fix the code or consciously change the decision.
A fitness function that has never failed has never told you anything.

## Running Them in CI

They are ordinary tests, so there is nothing special to wire up:

```yaml
- name: Architecture tests
  run: dotnet test --filter "FullyQualifiedName~ArchitectureTests"
```

No database, no HTTP, no containers.
The whole suite is one assembly parse plus in-memory graph work, so it belongs in the fast job that runs on every pull request, next to your [**unit tests**](https://milanjovanovic.tech/blog/unit-testing-best-practices-dotnet).

The change this makes to a team is smaller than it sounds and matters more than it sounds: an architecture violation stops being a review comment somebody has to write, notice, and win an argument about, and becomes a red build with the offending type name in it.

## When Rules Outgrow Code

Rules in code are the right default.
They live next to the tests, they refactor with the solution, and the full fluent vocabulary is available.

There is a point where that breaks down: when the same handful of rule shapes repeat across many targets, when people who do not build the solution need to author rules, or when the rules are content rather than configuration.
Then it is worth declaring rules as **data** and mapping them onto the fluent API:

```json
[
  { "id": "notifier-abstraction", "kind": "interface-must-exist",
    "interface": "INotifier" },
  { "id": "service-not-concrete", "kind": "type-must-not-reference",
    "type": "OrderService", "target": "SmtpNotifier" },
  { "id": "app-not-infra", "kind": "layer-must-not-depend-on",
    "from": "Shop.Application", "to": "Shop.Infrastructure" }
]
```

```csharp
public static ArchitectureRule Create(RuleDefinition definition) => definition.Kind switch
{
    "interface-must-exist"     => ArchRules.InterfaceMustExist(definition.Id, definition.Interface!),
    "type-must-not-reference"  => ArchRules.TypeMustNotReference(definition.Id, definition.Type!, definition.Target!),
    "layer-must-not-depend-on" => ArchRules.LayerMustNotDependOn(definition.Id, definition.From!, definition.To!),
    _ => throw new InvalidOperationException($"Unknown rule kind '{definition.Kind}'.")
};
```

Because an `ArchitectureRule` is ultimately just an id, a description, and an `IArchRule`, anything the data format cannot express drops down to a raw fluent rule in code, like the slice-cycle check above.
Data for the common shapes, the full vocabulary for the exotic ones.

That is not a trade most teams need to make.
Do not build it until the rules have actually multiplied.

## The Property That Makes This Safe

One implementation detail is worth knowing because it changes what you can point ArchUnitNET at: **it never executes the assembly.**

`ArchLoader` reads assemblies with [**Mono.Cecil**](https://github.com/jbevain/cecil), a static IL and metadata parser.
The assembly is never `Assembly.Load`ed into the runtime, so no module initializer runs, no static constructor fires, and nothing the code wants to do at load time happens.

For most teams that is a pleasant footnote: your fitness functions cannot be slowed down or broken by application startup code.

It stops being a footnote when the code is not yours.
[**Katabench**](https://katabench.com), my coding platform, has an architecture track where you refactor a small codebase (extract an abstraction, invert a dependency, break a cycle) and the platform grades the result.
Grading a refactoring means answering "is the structure right now?" mechanically, for code a stranger uploaded a second ago.
The submission is compiled with Roslyn, handed to Cecil, and evaluated as a set of fitness functions, all without a single line of it running.
Behavior gets judged separately, inside a sandbox, and both gates have to pass.

Static structure checking and behavioral testing being genuinely independent is not a grading trick.
It is why the shape check costs milliseconds and can run on every save while the slow gate runs later.

## Key Takeaways

**Start with three rules.**
You do not need a rule catalog to get value out of this.

1. Add one test project and load the architecture once in a static field.
2. Write the three rules you already state in onboarding: your most important dependency direction, the naming convention you keep repeating in reviews, and a slice-cycle rule over your feature namespaces.
3. Run them in the fast CI job.

Then let them fail.
The first failure is the useful one, because it tells you the gap between the architecture you describe and the one you have.

Architecture that is not executable erodes at exactly the speed your team ships.
If you want to see rules graded against a codebase in real time, the architecture track on [**Katabench**](https://katabench.com) does it every time you hit Run.

## Frequently asked questions

### What is an architecture fitness function?

An automated, executable check of a structural property of a system: dependency direction, layering, naming conventions, cycle freedom, library usage. The term comes from evolutionary architecture. Where a unit test asserts behavior, a fitness function asserts shape, and running one in CI turns an architectural decision from documentation into an enforced constraint.

### What is the difference between ArchUnitNET and NetArchTest?

Both assert architecture rules over compiled assemblies. NetArchTest is smaller and simpler, with a predicate-based API that covers the common dependency and naming checks. ArchUnitNET is a port of Java ArchUnit with a richer vocabulary: member-level rules, slice-based cycle detection, and per-violation evaluation results. Start with NetArchTest and move when you need something it cannot express.

### Can ArchUnitNET analyze an assembly without executing it?

Yes. ArchUnitNET loads assemblies with Mono.Cecil, which parses IL and metadata statically. The assembly is never loaded into the runtime, so no module initializer runs and no static constructor fires. That makes rule evaluation fast, side-effect free, and safe to run over assemblies you do not control.

### Do architecture tests slow down the build?

Barely, as long as you load the architecture once. Parsing the assemblies is the expensive step, so put the Architecture object in a static field shared across every test. After that each rule evaluates in memory with no database and no network, and a full suite finishes in seconds.

### Should architecture rules live in code or in configuration?

Code is the right default for a single codebase: the rules sit next to the tests and get refactored along with the solution. Declaring rules as data pays off when the same rule shapes repeat across many targets, when non-developers author them, or when rules ship as content. A small factory that maps declarative entries onto a shared rule vocabulary gives you both.
