Architecture Testing in .NET with NetArchTest

You've carefully designed your application with clean architecture — domain layer at the centre, application layer around it, infrastructure on the outside. Six months later, someone adds an Entity Framework reference to the domain project. The architecture diagram on the wiki still looks perfect, but the code has silently drifted.

Architecture tests encode your structural rules as automated tests. When someone violates a rule, the build breaks. NetArchTest is the most popular library for this in .NET, inspired by Java's ArchUnit.

Installation

Example.cs
dotnet add package NetArchTest.Rules

Basic Dependency Rules

The most common architectural rule: inner layers should not reference outer layers.

Example.cs
[Fact]
public void DomainLayer_ShouldNotDependOnInfrastructure()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn("MyApp.Infrastructure")
        .GetResult();

    Assert.True(result.IsSuccessful,
        $"Domain types depend on Infrastructure: {string.Join(", ", result.FailingTypeNames ?? Array.Empty<string>())}");
}

[Fact]
public void DomainLayer_ShouldNotDependOnApplication()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn("MyApp.Application")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

When this test fails, it tells you exactly which types are violating the rule — making the fix straightforward.

Enforcing Multiple Dependencies

Check that a layer doesn't reference any of several forbidden namespaces:

Example.cs
[Fact]
public void DomainLayer_ShouldNotDependOnExternalConcerns()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOnAny(
            "Microsoft.EntityFrameworkCore",
            "Microsoft.AspNetCore",
            "System.Net.Http",
            "MyApp.Infrastructure",
            "MyApp.Api")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Naming Conventions

Enforce that classes follow your team's naming conventions:

Example.cs
[Fact]
public void Controllers_ShouldEndWithController()
{
    var result = Types.InAssembly(typeof(Program).Assembly)
        .That()
        .Inherit(typeof(ControllerBase))
        .Should()
        .HaveNameEndingWith("Controller")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

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

    Assert.True(result.IsSuccessful);
}

Enforcing Interface Implementation

Ensure all repositories implement the expected interface:

Example.cs
[Fact]
public void Repositories_ShouldImplementIRepository()
{
    var result = Types.InAssembly(typeof(SqlOrderRepository).Assembly)
        .That()
        .HaveNameEndingWith("Repository")
        .Should()
        .ImplementInterface(typeof(IRepository<>))
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Class Attribute Rules

Enforce that all command handlers are sealed (a common performance and design recommendation):

Example.cs
[Fact]
public void CommandHandlers_ShouldBeSealed()
{
    var result = Types.InAssembly(typeof(CreateOrderHandler).Assembly)
        .That()
        .HaveNameEndingWith("Handler")
        .Should()
        .BeSealed()
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Namespace Rules

Ensure types live in the correct namespace based on their role:

Example.cs
[Fact]
public void Entities_ShouldResideInDomainNamespace()
{
    var result = Types.InAssembly(typeof(Order).Assembly)
        .That()
        .Inherit(typeof(Entity))
        .Should()
        .ResideInNamespace("MyApp.Domain.Entities")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Clean Architecture Test Suite

Here's a complete test class enforcing clean architecture boundaries:

Example.cs
public class ArchitectureTests
{
    private static readonly Assembly DomainAssembly = typeof(Order).Assembly;
    private static readonly Assembly ApplicationAssembly = typeof(CreateOrderCommand).Assembly;
    private static readonly Assembly InfrastructureAssembly = typeof(AppDbContext).Assembly;
    private static readonly Assembly ApiAssembly = typeof(Program).Assembly;

    [Fact]
    public void Domain_ShouldNotDependOnOtherLayers()
    {
        var result = Types.InAssembly(DomainAssembly)
            .ShouldNot()
            .HaveDependencyOnAny(
                "MyApp.Application",
                "MyApp.Infrastructure",
                "MyApp.Api")
            .GetResult();

        Assert.True(result.IsSuccessful);
    }

    [Fact]
    public void Application_ShouldNotDependOnInfrastructureOrApi()
    {
        var result = Types.InAssembly(ApplicationAssembly)
            .ShouldNot()
            .HaveDependencyOnAny(
                "MyApp.Infrastructure",
                "MyApp.Api")
            .GetResult();

        Assert.True(result.IsSuccessful);
    }

    [Fact]
    public void Infrastructure_ShouldNotDependOnApi()
    {
        var result = Types.InAssembly(InfrastructureAssembly)
            .ShouldNot()
            .HaveDependencyOn("MyApp.Api")
            .GetResult();

        Assert.True(result.IsSuccessful);
    }
}

Running These Tests

Architecture tests run as part of your regular test suite — dotnet test executes them alongside your unit and integration tests. They're fast (they analyse compiled assemblies, not source code) and deterministic.

Add them to your CI pipeline and they become an automated architecture review. No more relying on code reviews to catch dependency violations — the tests catch them before the PR is even raised.

When to Add Architecture Tests

Start with the most critical rules — typically dependency direction between layers. Add naming convention tests when you notice inconsistencies creeping in. Don't try to encode every possible rule up front; let violations in code reviews guide which tests to add next.

The goal isn't to be prescriptive about every detail. It's to protect the structural decisions that are expensive to fix after they've been violated across dozens of files.