In this article, we’ll explore how to use the NetArchTest.Rules library to write architecture tests for our .NET applications.
Let’s dive in!
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
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.
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.
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.
Ensure Correct Implementation or Inheritance
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
Software architectures such as either Onion or Clean architecture have strict rules when it comes to project dependencies. Let’s create a test to verify these strict 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.
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.
Conclusion
In this article, we delved into the effective utilization of the NetArchTest.Rules library to craft architecture tests specifically tailored to .NET applications. By enforcing conventions on class design, naming, and dependencies, we ensure the integrity of a given software architecture. Notably, the library provides a robust framework for writing effective architecture tests, addressing various aspects from preventing unwanted inheritance to validating project references and implementing custom rules. Furthermore, a significant feature is the ability to create policies, that enhance its flexibility and enable us to tailor tests to meet specific project requirements. Through the integration of NetArchTest.Rules into our testing workflow, we can confidently apply architectural best practices, fostering a resilient and well-structured codebase.


