Enforcing .NET Architecture with NetArchTest

Maintaining consistent architecture in a .NET codebase is harder than it looks. Everyone starts with good intentions, and then the real world happens. Deadlines show up, a hotfix needs to land, a new team member copies a pattern from a random file, and suddenly your “clean” boundaries are more of a suggestion.

Code reviews help, docs help, and team agreement helps, but none of those are a guarantee. If you want architecture rules to actually stick over time, you need something that fails loudly when the rules are broken.

The Solution: Automated Architecture Testing

The idea behind architecture tests is simple: treat architectural decisions like any other behavior you care about. If you’re serious about “API does not reference data” or “controllers don’t touch repositories”, encode that in tests.

These tests live alongside your unit and integration tests. They run in CI. They fail the build. No debates, no “we’ll clean it up later”, no slow drift.

What is NetArchTest?

NetArchTest is a fluent API for .NET (created by Ben Morris) that lets you define and enforce architectural rules directly in tests. If you’ve seen the Java library ArchUnit, this will feel familiar.

The API reads like English: load some types, filter them down, then assert conditions about those types.

Writing Rules with NetArchTest

The typical flow looks like this:

  • Load types from an assembly (or current AppDomain)
  • Filter down to what you care about (namespace, interfaces, classes, etc.)
  • Apply Should() or ShouldNot() assertions
  • Evaluate with GetResult()

Here’s a basic example using xUnit.

using NetArchTest.Rules;
using Xunit;

public class ArchitectureTests
{
    [Fact]
    public void Controllers_Should_Not_Depend_On_DataLayer()
    {
        var result = Types.InAssembly(typeof(ArchitectureTests).Assembly)
            .That()
            .ResideInNamespace("MyApp.Api")
            .And()
            .HaveNameEndingWith("Controller")
            .ShouldNot()
            .HaveDependencyOn("MyApp.Data")
            .GetResult();

        Assert.True(result.IsSuccessful);
    }
}

A couple notes from experience:

  • If you are enforcing a layered architecture, HaveDependencyOn("MyApp.Data") is a great early warning system.
  • You don’t need to be perfect on day one. Start with 2 or 3 rules that prevent the biggest architectural leaks.

Another handy rule: naming and conventions

A lot of architecture drift is really convention drift. For example, you might want to enforce “interfaces start with I”. It’s small, but it keeps the codebase consistent.

[Fact]
public void Interfaces_Should_Start_With_I()
{
    var result = Types.InAssembly(typeof(ArchitectureTests).Assembly)
        .That()
        .AreInterfaces()
        .Should()
        .HaveNameStartingWith("I")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Custom Rules

The built-in predicates and conditions are usually enough, but sometimes you want to encode something specific to your organization or architecture style.

NetArchTest supports custom rules via the ICustomRule interface, and you can apply them with MeetCustomRule().

This is a good fit for things like:

  • “Only types in this namespace can use this dependency”
  • “Handlers must be internal”
  • “Only these assemblies can reference our shared kernel”

Policies: Grouping Multiple Rules

Once you have more than a couple rules, you’ll want a clean way to run them as a set. NetArchTest provides a Policy API for that.

A policy lets you:

  • Define multiple rules
  • Provide a name/description for each rule
  • Evaluate them together

This example is adapted from the NetArchTest README:

using NetArchTest.Rules;
using Xunit;

public class PolicyTests
{
    [Fact]
    public void ArchitecturePolicy_Should_Pass()
    {
        var architecturePolicy = Policy.Define("Architecture", "High-level architectural constraints")
            .For(Types.InCurrentDomain)
            .Add(t =>
                    t.That()
                     .ResideInNamespace("MyApp.Api")
                     .ShouldNot()
                     .HaveDependencyOn("MyApp.Data"),
                 "Layering", "API should not directly reference Data")
            .Add(t =>
                    t.That()
                     .AreInterfaces()
                     .Should()
                     .HaveNameStartingWith("I"),
                 "Conventions", "Interface names should start with 'I'");

        var results = architecturePolicy.Evaluate();

        Assert.True(results.HasViolations == false);
    }
}

That gives you one test that reports a full set of violations, instead of a bunch of scattered asserts.

Getting Started

If you want to try this out, start small:

  • Add NetArchTest.Rules to your test project
  • Pick one boundary you care about (often API -> Data is the big one)
  • Add 1 test
  • Run it in CI and treat failures seriously

Conclusion

Architecture tests are one of the easiest ways to keep a .NET codebase from slowly melting into a ball of mud. You still need good design and good reviews, but automated rules give you a safety net that doesn’t get tired or distracted.

If your team has ever said “we’ll refactor later”, this is one of the better ways to make sure later actually happens.

Avatar
Alan P. Barber
Software Developer, Computer Scientist, Scrum Master, & Crohn’s Disease Fighter

I specialize in Software Development with a focus on Architecture and Design.

comments powered by Disqus
Next
Previous

Related