Maintain Clean Architecture Rules with Architecture Tests

You are currently viewing Maintain Clean Architecture Rules with Architecture Tests

In a large-scale application, maintaining a well-structured codebase is crucial for scalability, maintainability, and long-term success. Over time, as teams grow and features evolve, the violations of architectural rules can creep in, leading to tightly coupled, hard-to-maintain code. This is where architecture tests come in—they act as automated checks to ensure that all the team members follow the predefined design principles. Architecture tests help prevent technical debt and keep the codebase aligned with best practices. In this post, we’ll explore how to write architecture tests using the NetArchTest library to safeguard .NET applications built using clean architecture.

Overview of Clean Architecture

Clean Architecture is a software design pattern that promotes the separation of concerns, ensuring that business logic remains independent of external frameworks and infrastructure. It organizes code into distinct layers:

  • Domain Layer (Core business logic, independent of frameworks).
  • Application Layer (Use cases, orchestrating domain logic).
  • Infrastructure Layer (Database, external services, and persistence).
  • Presentation Layer (UI, API controllers, handling user interactions).

Dependencies flow inward, meaning outer layers (like Infrastructure and Presentation) depend on inner layers (Domain and Application), but not vice versa. This approach enhances testability, maintainability, and flexibility, making it easier to adapt to new technologies without affecting core business rules.

If you want to learn more about clean architecture, then you can read my following posts that explain clean architecture in detail.

Common Architectural Rules to Enforce

When writing architecture tests for a .NET application following Clean Architecture, some common rules to enforce include:

  1. Layer Dependency Rules – Ensure that dependencies flow inward (e.g., the Domain layer should not depend on Application, Infrastructure, or Presentation).
  2. No Direct Infrastructure Access – Application and Domain layers should not reference Infrastructure directly.
  3. Restricted Framework Dependencies – The domain layer should not depend on external libraries (e.g., EF Core, ASP.NET Core).
  4. Naming Conventions – Enforce naming patterns for repositories, services, and interfaces.
  5. Encapsulation of Business Logic – Business rules should reside in the Domain layer, not in controllers or services.
  6. Interfaces Implementation – Repositories and services should adhere to predefined interfaces.
READ ALSO:  Distributed Caching in ASP.NET Core using Redis Cache

Introduction to NetArchTest Library

NetArchTest is a lightweight library for writing architecture tests in .NET applications. It provides a fluent API to define and enforce architectural rules, ensuring that a project adheres to best practices. With NetArchTest, you can verify dependencies between layers, enforce naming conventions, and ensure that certain types adhere to specific constraints (e.g., ensuring repositories implement an interface or that domain models do not depend on infrastructure).

Key Features of NetArchTest

  1. Dependency Validation – Ensures that different layers follow correct dependency rules (e.g., preventing Infrastructure from referencing Domain).
  2. Type Filtering – Allows selecting classes based on naming patterns, attributes, or implemented interfaces.
  3. Fluent API for Defining Rules – Provides an intuitive way to write architecture rules in unit tests.
  4. Test Framework Compatibility – Works seamlessly with popular test frameworks like xUnit and NUnit.
  5. Preventing Layer Violations – Helps enforce Clean Architecture by ensuring correct layer separation.
  6. Automated Enforcement – Can be integrated into CI/CD pipelines to prevent unintentional architecture violations.

Setting Up the Clean Architecture Project

Before we install and start writing architecture tests, we need to have a few projects that are following clean architecture principals. For the purpose of this article let’s create following projects of a simple online blogging application.

  • OnlineBlog.Domain
  • OnlineBlog.Application
  • OnlineBlog.Infrastructure

Implement the following Article domain entity in the OnlineBlog.Domain project.

Article.cs

public sealed class Article
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }

    private Article(string title, string body)
    {
        Id = Guid.NewGuid();
        Title = title;
        Body = body;
    }

    private Article() 
    { }

    public static Article CreateArticle(string title, string body)
    {
        return new Article(title, body);
    }
}

Let’s also implement the following IArticleRepository in the OnlineBlog.Domain project.

IArticleRepository.cs

public interface IArticleRepository : IRepository
{
    void Add(Article article);
    Task SaveChangesAsync(CancellationToken cancellationToken);
}

Next, implement the following CreateArticleCommand and CreateArticleCommandHandler classes in OnlineBlog.Application project that use CQRS pattern using very popular MediatR library. If you want to learn more about CQRS, read my article Implement CQRS Pattern in ASP.NET Core 5.

CreateArticleCommand.cs

public record CreateArticleCommand(string Title, string Body) : IRequest<bool>;

CreateArticleCommandHandler.cs

public class CreateArticleCommandHandler(IArticleRepository articleRepository) : IRequest<bool>
{
    public async Task<bool> Handle(CreateArticleCommand request, CancellationToken cancellationToken)
    {
        var article = Article.CreateArticle(request.Title, request.Body);
        articleRepository.Add(article);
        await articleRepository.SaveChangesAsync(cancellationToken);
        return true;
    }
}

Finally, create the following ArticleRepository in OnlineBlog.Architecture project.

READ ALSO:  Building ASP.NET Core Apps with Clean Architecture

ArticleRepository.cs

public class ArticleRepository(BloggerDbContext bloggerDbContext) : IArticleRepository
{
    public void Add(Article article)
    {
        bloggerDbContext.Articles.Add(article);
    }

    public async Task SaveChangesAsync(CancellationToken cancellationToken)
    {
        await bloggerDbContext.SaveChangesAsync(cancellationToken);
    }
}

Installing and Configuring NetArchTest Library

Since NetArchTest is used to write architecture tests, it should be added to a test project. Create a new xUnit Test Project in Visual Studio 2022 using .NET 9 and add the NetArchTest in the project using .NET CLI or Nuget Package Manager.

Using .NET CLI:

dotnet add package NetArchTest.Rules

Using NuGet Package Manager:

Install-Package NetArchTest.Rules

Writing Architecture Tests using NetArchTest

We are now ready to write some architecture tests in the same way we write any unit test in our applications. The starting point for writing architecture tests is the static Types class, which we can use to load a set of types e.g. classes or interfaces.

Once we have loaded our types we can further filter them to find a more specific set of types.

Some of the available filtering methods:

  • ResideInNamespace
  • AreClasses
  • AreInterfaces
  • HaveNameStartingWith
  • HaveNameEndingWith

Finally, when we are satisfied with our selection, we can write the rule we want to enforce by calling Should or ShouldNot and applying the condition we want to check.

Here’s an example of our first architecture test that checks all classes in the OnlineBlog.Domain assembly are sealed (i.e., cannot be inherited). This helps enforce immutability and encapsulation, which are important principles in Domain-Driven Design (DDD).

public class ArchitectureTests
{
    private Assembly DomainAssembly = typeof(Article).Assembly;

    [Fact]
    public void Domain_Classes_Should_Be_Sealed()
    {
       var result = Types
            .InAssembly(DomainAssembly)
            .That().AreClasses()
            .Should().BeSealed()
            .GetResult();

       Assert.True(result.IsSuccessful, "All Domain classes must be sealed.");
    }
}

Here is the step-by-step breakdown of the above code snippet.

The following line declares the DomainAssembly variable that stores the reference to typeof(Article).Assembly, ensuring the correct assembly is tested.

private Assembly DomainAssembly = typeof(Article).Assembly;

The following line loads all types (classes, interfaces, etc.) from the Domain assembly. This ensures that the test is applied to the entire Domain layer.

Types
   .InAssembly(DomainAssembly)

The following line filters only class types (excluding interfaces, enums, structs, etc.).

.That().AreClasses()

The following line specifies that all selected classes must be sealed . A sealed class cannot be inherited, which is often recommended for domain entities and value objects in DDD to maintain integrity and prevent unintended modifications.

.Should().BeSealed()

The following line runs the architecture test and returns a TestResult object, which contains the test outcome.

.GetResult();

Finally, we are asserting the test result using the following line.

Assert.True(result.IsSuccessful, "All Domain classes must be sealed.");

If all classes are sealed, the test will pass (result.IsSuccessful == true). If any class is not sealed, the test will fail, indicating a violation of the architectural rule.

READ ALSO:  Using C# Delegates with Events

Enforce Naming Conventions in Architecture Tests

Let’s say you want to enforce a naming convention to ensure that repository classes in the Infrastructure layer follow the pattern “Repository” in their names. You can write an architecture test similar to the following.

private Assembly InfrastructureAssembly = typeof(ArticleRepository).Assembly;

[Fact]
public void Repository_Classes_Should_End_With_Repository()
{
    var result = Types
        .InAssembly(InfrastructureAssembly)
        .That()
        .ResideInNamespace("OnlineBlog.Infrastructure.Repositories")
        .And()
        .AreClasses()
        .Should()
        .HaveNameEndingWith("Repository")
        .GetResult();

    Assert.True(result.IsSuccessful, "All repository classes should end with 'Repository'.");
}

Ensuring Layer Dependencies in Architecture Tests

One interesting aspect of the NetArchTest library is the ability to define slices or layers. This can be particularly useful to enforce software architecture rules in a layered architecture. Let’s take the example of the Clean architecture:

  • Domain should not have any dependencies
  • Application should not depend on Infrastructure
  • Infrastructure should depend on the Application and Domain

Here’s how you can write tests for enforcing architecture rules.

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

    Assert.True(result.IsSuccessful, "Domain layer should not depend on Application or Infrastructure layers.");
}

The above code can also be written as two separate tests using the HaveDependencyOn method. The following test checks that Domain layer should not have any dependency on Application layer.

var result = Types.InAssembly(DomainAssembly)
    .That()
    .ResideInNamespace("OnlineBlog.Domain")
    .ShouldNot()
    .HaveDependencyOn("OnlineBlog.Application")
    .GetResult();

Assert.True(result.IsSuccessful, "Domain layer should not depend on Application layer.");

The following test checks that Domain layer should not have any dependency on Infrastructure layer.

var result = Types.InAssembly(DomainAssembly)
    .That()
    .ResideInNamespace("OnlineBlog.Domain")
    .ShouldNot()
    .HaveDependencyOn("OnlineBlog.Infrastructure")
    .GetResult();

Assert.True(result.IsSuccessful, "Domain layer should not depend on Infrastructure layer.");

Ensuring Interface Implementation Rules in Architecture Tests

Interface implementation rule enforces that all classes in a particular namespace or assembly follow a standard contract by implementing a particular interface.

The following architecture test ensures that all classes in the OnlineBlog.Infrastructure.Repositories namespace implement the IRepository interface.

[Fact]
public void All_Repositories_Should_Implement_IRepository()
{
    var result = Types.InCurrentDomain()
        .That()
        .ResideInNamespace("OnlineBlog.Infrastructure.Repositories")
        .And()
        .AreClasses()
        .Should().ImplementInterface(typeof(IRepository))
        .GetResult();

    Assert.True(result.IsSuccessful, "Domain repositories in Infrastructure layer should implement IRepository.");
}

Conclusion

Manually enforcing software architecture rules with pair programming or constant PR reviews is error error-prone and time-consuming task. Architecture tests play a crucial role in maintaining a well-structured, scalable, and maintainable application. NetArchTest library allows developers to enforce architectural rules, prevent unwanted dependencies, and ensure consistency across layers with automated tests. These tests act as a safety net, catching violations early in the development process and preventing architecture drift over time. Integrating architecture tests into your CI/CD pipeline further strengthens your codebase by ensuring compliance with best practices.

Leave a Reply