Updated on
NetArchTest is a small .NET library that turns architecture rules into unit tests. We write a normal xUnit or NUnit test that asserts something about our own assemblies, for example “nothing in Domain references Persistence” or “every class in Persistence is sealed”, and it fails the build when someone breaks the rule.
Its author, Ben Morris, describes it as “a fluent API for .Net Standard that can enforce architectural rules in unit tests”, inspired by Java’s ArchUnit (BenMorris/NetArchTest, read 2026-08-09). It ships as the NetArchTest.Rules NuGet package, has a fluent API built around a single Types entry point, and needs no runner, no configuration file, and no changes to the projects being tested.
What Is NetArchTest?
NetArchTest is an open-source .NET library for writing architecture tests: tests whose assertions are about our code’s structure rather than its behaviour.
It works by reflecting over compiled assemblies. We hand it an assembly, filter down to a set of types with That(), state an expectation with Should() or ShouldNot(), and call GetResult() to get back a pass or fail plus the list of offending types.
Because the result is an ordinary boolean assertion, these tests live in the same xUnit, NUnit, or MSTest project as everything else and fail the same build. There is no separate runner and no configuration to maintain.
The typical use is protecting a layered design. Onion and Clean architectures both depend on a dependency rule that the compiler only partly enforces, and a project reference added in a hurry can quietly invert it. One architecture test catches that on the next CI run instead of during a review months later.
The package is NetArchTest.Rules, and the project lives at github.com/BenMorris/NetArchTest.
The project states the same scope: architecture tests “can be used with any unit test framework and incorporated into a build pipeline” (BenMorris/NetArchTest, read 2026-08-09).
Creating the Base for Architecture Tests in .NET
For this article, we’re going to use a simple project based on the Onion architecture:
We create several projects representing the different layers of this architectural pattern.
Firstly, the Domain layer consists of the Domain project. Moving on to the Service layer, we find the implementation split between the Services and Services.Abstractions projects. Transitioning to the Infrastructure layer, we have the Persistence project. Lastly, the Api project represents the Presentation layer.
Let’s create our base entity:
public class Cat
{
public required Guid Id { get; set; }
public required string Name { get; set; }
public required string Breed { get; set; }
public required DateTime DateOfBirth { get; set; }
}
Here, we create the Cat class around which our application is built. We add, update, delete, and query cats via the Api project.
Now that we’ve got our projects ready to test, let’s proceed by creating a new xUnit test project and install the required NuGet package:
dotnet add package NetArchTest.Rules
For more on the framework itself, see our guide to unit testing with xUnit in ASP.NET Core, or compare it with the alternatives in differences between NUnit, xUnit, and MSTest.
We can find the main rules for architecture tests in the NetArchTest.Rules package. NetArchTest itself is a library that allows us to create tests that enforce conventions for class design, naming, and dependency in .NET.
The library itself has been stable since 2021 – NetArchTest.Rules 1.3.2 (published 2021-05-23) is the current release and nothing has shipped since, though the project still builds against modern .NET and a few actively maintained forks exist if that ever becomes a blocker.
Now that we have everything ready, let’s start testing!
Architecture Tests to Prevent Inheritance in .NET
We know that we want the classes in the Persistence layer to be marked as sealed, thus ensuring that they cannot be inherited. Let’s see how we can enforce this:
[Fact]
public void GivenPersistenceLayerClasses_WhenInheritanceIsAttempted_ThenItShouldNotBePossible()
{
// Arrange
var persistenceLayerAssembly = typeof(CatsDbContext).Assembly;
// Act
var result = Types.InAssembly(persistenceLayerAssembly)
.Should().BeSealed()
.GetResult();
// Assert
result.IsSuccessful.Should().BeTrue();
}
Firstly, we get hold of the assembly for our Persistence project. Then, we start by using the Types class which is the entry point to the NetArchTest.Rules library. We move on to pass the persistenceLayerAssembly variable to the InAssembly() method – this will get all the types in that assembly.
Secondly, we call the Should() method. With it, we apply conditions to the list of matching types we have so far. In our case, the only condition we have is that all classes must be sealed, so we use the BeSealed() method. We finish off by calling the GetResult() method.
Finally, we get a variable result having the TestResult type. It has one boolean IsSuccessful property that states whether or not all types match our conditions. Therefore, using the FluentAssertions library, we ensure that the IsSuccessful property returns true.
The sample pins FluentAssertions to 6.12.0, the last version under the Apache-2.0 licence – from v8 the package moved to Xceed’s Community Licence, which is free for personal use but requires a paid tier for commercial use, so we pin the dependency deliberately rather than picking up the latest version blind. If we’d rather sidestep the question entirely, Assert.True(result.IsSuccessful) works identically with no extra dependency, or Shouldly for more readable assertions is a free alternative worth a look.
Architecture Tests That Enforce Class Visibility in .NET
In Onion architecture, concrete service implementations should not be visible outside the Service layer.
This doesn’t apply to the interfaces in the Services.Abstractions projects and all of them should be public, so let’s set up a test for that:
[Fact]
public void GivenServiceLayerInterfaces_WhenAccessedFromOtherProjects_ThenTheyAreVisible()
{
// Arrange
var serviceLayerAssembly = typeof(IServiceManager).Assembly;
// Act
var result = Types.InAssembly(serviceLayerAssembly)
.Should().BePublic()
.GetResult();
// Assert
result.IsSuccessful.Should().BeTrue();
}
Using the IServiceManager type, we get the assembly where all the interface types in the Service layer reside. Then, we use the BePublic() method, which is the condition we are after. In the end, we assert that the result is successful.
Architecture Tests That Enforce Interface Implementation in .NET
We can easily ensure that our classes either implement the correct interfaces or inherit from the desired classes:
[Fact]
public void GivenCatNotFoundException_ThenShouldInheritFromNotFoundException()
{
// Arrange
var domainLayerAssembly = typeof(CatNotFoundException).Assembly;
// Act
var result = Types.InAssembly(domainLayerAssembly)
.That().ResideInNamespace("Domain.Exceptions")
.And().HaveNameStartingWith("Cat")
.Should().Inherit(typeof(NotFoundException))
.GetResult();
// Assert
result.IsSuccessful.Should().BeTrue();
}
Here, we create a test to ensure that the CatNotFoundException class inherits from the NotFoundException class. After specifying the assembly, we use the That() method to specify to which types our conditions must apply. We use the ResideInNamespace() method to get all types in the Domain.Exceptions namespace.
This will get types in any child namespaces, so we add the And() method to add further filtering and continue with the HaveNameStartingWith() method. Only our CatNotFoundException matches all filters and we continue with the Should() and Inherit() methods. Finally, we assert that the result is true, confirming that our inheritance rules are abided.
Architecture Tests That Enforce Project References in .NET
Onion and Clean architecture both put strict rules on project dependencies. Let’s create a test to verify those rules:
[Fact]
public void GivenDomainLayer_ThenShouldNotHaveAnyDependencies()
{
// Arrange
var domainLayerAssembly = typeof(Cat).Assembly;
// Act
var result = Types.InAssembly(domainLayerAssembly)
.ShouldNot().HaveDependencyOnAll(
["Api", "Contracts", "Persistence", "Services", "Services.Abstractions"])
.GetResult();
// Assert
result.IsSuccessful.Should().BeTrue();
}
First, we write a test to ensure that our Domain project doesn’t have any dependencies on the rest of the projects in our solution. Then, we get the required assembly and use the ShouldNot() method together with the HaveDependencyOnAll() method. To the latter, we pass all other namespaces as strings using collection expressions.
Note that many of the conditional methods in NetArchTest.Rules have two versions – either starting with Have or with NotHave. This, alongside the Should() and ShouldNot() methods, enables us to write tests in two different ways, achieving the same result.
If layering by architecture doesn’t fit every team, vertical slice architecture in ASP.NET Core organizes code by feature instead, and the same NetArchTest approach still applies to enforcing its boundaries.
Writing a Custom Rule for Architecture Tests in .NET
The NetArchTest.Rules library allows us to create custom rules as well:
public class CustomServiceLayerRule : ICustomRule
{
public bool MeetsRule(TypeDefinition type)
=> type.IsInterface && type.IsPublic && type.Name.StartsWith('I');
}
To create a custom rule, we need to implement the ICustomRule interface and it’s MeetsRule() method. We create a rule to ensure that all types in the Services.Abstractions project are interfaces, have the public access modifier and the name is prefixed with I.
Now, let’s use our rule:
[Fact]
public void GivenServiceInterfaces_ThenShouldBePublicAndBeInterfacesAndStartWithI()
{
// Arrange
var serviceLayerAssembly = typeof(IServiceManager).Assembly;
var myCustomRule = new CustomServiceLayerRule();
// Act
var result = Types.InAssembly(serviceLayerAssembly)
.Should().MeetCustomRule(myCustomRule)
.GetResult();
// Assert
result.IsSuccessful.Should().BeTrue();
}
In our test, we first create an instance of our CustomServiceLayerRule class. Then, we pass that instance to the MeetCustomRule() method. This will apply this rule to all matching types, in our case those are all types in the Services.Abstractions project.
Defining Custom Rules Policies for Architecture Tests in .NET
The NetArchTest.Rules library allows us to utilize custom policies that combine several different rules:
[Fact]
public void GivenServiceInterfaces_ThenShouldMeetCustomRuleAndServiceManagerShouldHaveDependencyOnContracts()
{
// Arrange
var serviceLayerAssembly = typeof(IServiceManager).Assembly;
var myCustomRule = new CustomServiceLayerRule();
var customPolicy = Policy.Define(
"Service Interfaces Policy",
"This policy ensures that all types meet the given conditions")
.For(Types.InAssembly(serviceLayerAssembly))
.Add(types => types
.Should().MeetCustomRule(myCustomRule))
.Add(types => types
.That().HaveNameEndingWith("Manager")
.ShouldNot().HaveDependencyOn("Contracts")
);
// Act
var results = customPolicy.Evaluate();
// Assert
foreach (var result in results.Results)
{
result.IsSuccessful.Should().BeTrue();
}
}
Firstly, we start to create a custom policy by calling the Define() method on the Policy type. The method takes two parameters – a name and a description. We continue the policy creation with the For() method in which we specify which types we are going to test.
Next, we move on to defining rulesets for different sub-types. We do this by chaining Add() methods, in which we filter types and use the Should() and ShouldNot() methods to assert given conditions.
After our policy is done, we invoke the Evaluate() method to get a list of TestResult object. Finally, we assert that the IsSuccessful property of each result is true, ensuring all conditions have been met.
| We want to enforce | Rule method | Example |
|---|---|---|
| Classes cannot be inherited | BeSealed() | Types.InAssembly(a).Should().BeSealed() |
| Types stay internal | NotBePublic() | .That().ResideInNamespace("X.Internal").Should().NotBePublic() |
| A layer has no dependency on another | NotHaveDependencyOn() | .That().ResideInNamespace("Domain").ShouldNot().HaveDependencyOn("Persistence") |
| Types implement an interface | ImplementInterface() | .That().HaveNameEndingWith("Repository").Should().ImplementInterface(typeof(IRepository)) |
| Naming convention | HaveNameEndingWith() | .That().Inherit(typeof(ControllerBase)).Should().HaveNameEndingWith("Controller") |
| Types live in the right namespace | ResideInNamespace() | .That().ImplementInterface(typeof(IHandler)).Should().ResideInNamespace("Application") |
| Several rules as one unit | Policy.Define() | Policy.Define("Layering", "...").For(a).Add(...) |
Why Do We Write Architecture Tests in .NET?
Architecture decisions decay quietly. The dependency rule that everyone agreed to at the start of the project survives exactly as long as someone remembers to check it in code review.
An architecture test moves that check from human attention to the build. Adding a project reference from Domain to Persistence stops being a review comment somebody might miss and becomes a red test with the offending type named in the failure message.
The second benefit is onboarding. A new developer reading ShouldNot().HaveDependencyOn("Persistence") learns the rule and its rationale in one line, without finding the architecture decision record that explains it.
The cost is real but small: these tests run against compiled assemblies, so they need a build first, and an overly strict rule becomes a nuisance that people disable rather than satisfy.
The rule of thumb is to encode the constraints we would actually reject a pull request over (layering and naming) and leave taste to review.
Conclusion
In this article, we used NetArchTest.Rules to write architecture tests for .NET applications. We enforced conventions on class design, naming, and dependencies: blocking unwanted inheritance, checking project references, and adding custom rules of our own. We also grouped rules into policies, which lets us shape a suite around one project’s constraints. With those tests in the build, the architecture we agreed on stays the architecture we ship.
Tested with .NET 10.0.10.

