Enforce C# Architecture Rules with NetArchTest
This article explains how to implement automated architecture tests in .NET using the NetArchTest library to enforce architectural boundaries and dependency rules. You will learn how to set up an architecture testing project, define rules that prevent unwanted dependencies between layers—such as Domain, Application, and Infrastructure—and seamlessly integrate these assertions into standard unit testing frameworks like xUnit or NUnit.
Why Use Architecture Testing?
In layered or Clean Architecture solutions, dependency rules dictate that high-level modules should not depend on low-level modules. Over time, developers may inadvertently introduce illegal project references or cross-namespace dependencies. NetArchTest solves this problem by analyzing compiled assemblies using reflection and Mono.Cecil, allowing you to write automated tests that fail during the build process if a dependency boundary is violated.
Installation and Setup
To get started, create a standard unit test project (xUnit, NUnit, or
MSTest) within your solution and install the
NetArchTest.Rules NuGet package.
Using the .NET CLI:
dotnet add package NetArchTest.RulesEnsure your test project references the projects or assemblies you intend to test.
Defining Core Architectural Rules
NetArchTest operates through a fluent API centered around the
Types class. You select types from an assembly, filter them
based on conditions, and assert dependency expectations.
Example: Enforcing Clean Architecture Layer Dependencies
In a typical Clean Architecture solution, the Domain layer must have no dependencies on the Application, Infrastructure, or Web layers. The Application layer can depend on the Domain, but not on the Infrastructure layer.
Here is how to enforce these rules using xUnit:
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
private const string DomainNamespace = "MyApp.Domain";
private const string ApplicationNamespace = "MyApp.Application";
private const string InfrastructureNamespace = "MyApp.Infrastructure";
private const string WebApiNamespace = "MyApp.WebApi";
[Fact]
public void DomainLayer_ShouldNotHaveDependencyOnOtherProjects()
{
// Arrange
var assembly = typeof(MyApp.Domain.AssemblyReference).Assembly;
var otherProjects = new[]
{
ApplicationNamespace,
InfrastructureNamespace,
WebApiNamespace
};
// Act
var result = Types.InAssembly(assembly)
.ShouldNot()
.HaveDependencyOnAny(otherProjects)
.GetResult();
// Assert
Assert.True(result.IsSuccessful, "Domain layer cannot depend on external layers.");
}
[Fact]
public void ApplicationLayer_ShouldNotDependOnInfrastructureOrWeb()
{
// Arrange
var assembly = typeof(MyApp.Application.AssemblyReference).Assembly;
var forbiddenProjects = new[]
{
InfrastructureNamespace,
WebApiNamespace
};
// Act
var result = Types.InAssembly(assembly)
.ShouldNot()
.HaveDependencyOnAny(forbiddenProjects)
.GetResult();
// Assert
Assert.True(result.IsSuccessful, "Application layer cannot depend on Infrastructure or Web layers.");
}
}Enforcing Design Patterns and Class Conventions
NetArchTest can also enforce design patterns, naming conventions, and inheritance rules across your solution.
Enforcing Repository Implementation Location
You can ensure that interfaces ending in Repository are
implemented exclusively within the Infrastructure project:
[Fact]
public void Repositories_ShouldResideInInfrastructureLayer()
{
var assembly = typeof(MyApp.Infrastructure.AssemblyReference).Assembly;
var result = Types.InAssembly(assembly)
.That()
.HaveNameEndingWith("Repository")
.And()
.AreClasses()
.Should()
.ResideInNamespace("MyApp.Infrastructure.Repositories")
.GetResult();
Assert.True(result.IsSuccessful);
}Enforcing Controller Dependencies
You can prevent API controllers from directly depending on database contexts or repositories, forcing them to communicate through command handlers or services:
[Fact]
public void Controllers_ShouldNotDirectlyReferenceDatabase()
{
var assembly = typeof(MyApp.WebApi.AssemblyReference).Assembly;
var result = Types.InAssembly(assembly)
.That()
.HaveNameEndingWith("Controller")
.ShouldNot()
.HaveDependencyOn("MyApp.Infrastructure.Data")
.GetResult();
Assert.True(result.IsSuccessful, "Controllers should not reference the database directly.");
}Handling Violations and Debugging
When result.IsSuccessful is false, the
TestResult object provides the names of the offending types
through the FailingTypeNames property. To make debugging
easier when a test fails, format your assertions to display these
types:
[Fact]
public void Handlers_ShouldHaveNameEndingWithHandler()
{
var assembly = typeof(MyApp.Application.AssemblyReference).Assembly;
var result = Types.InAssembly(assembly)
.That()
.ImplementInterface(typeof(IRequestHandler<,>))
.Should()
.HaveNameEndingWith("Handler")
.GetResult();
Assert.True(
result.IsSuccessful,
$"Violating types: {string.Join(", ", result.FailingTypeNames ?? Enumerable.Empty<string>())}"
);
}Integration into the CI Pipeline
Because these architecture rules are written as standard unit tests,
they run automatically via the dotnet test command. Adding
them to your continuous integration (CI) pipeline ensures that no pull
request containing architectural drift or circular dependencies can be
merged into your main branch.