👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Fitness Test using Net Arch Test in ASP.NET WEB API

Fitness Test using Net Arch Test in ASP.NET WEB API

Author - Abdul Rahman (Bhai)

Web API

23 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

In the realm of software development, maintaining a robust and consistent architecture is paramount. As projects expand, ensuring that the structural integrity of the codebase remains intact becomes increasingly challenging. Even as of writing this I joined a project which was started six months ago with Clean Architecture in mind, but now it's follows a name sake Clean Architecture and the architecture is already eroding. The codebase is riddled with inconsistencies, and the architecture is far from the initial vision. This is because of pressure and deadline and on the other hand new developers joining the team without knowledge of clean architecture. This is a common problem in software development.

This is where fitness test or fitness functions come into play, serving as vigilant gatekeepers, preserving the essence of your software blueprint. In this blog post, we will delve deep into NetArchTest, a powerful tool designed to validate your software's architectural design, exploring its significance, implementation, advantages, and more.

NetArchTest is a sophisticated library that empowers developers to write fitness tests effortlessly. By leveraging NetArchTest, developers can enforce architectural rules and validate design decisions. The library scans the imported namespaces of your types, allowing you to define both positive and negative conditions for your architecture. Whether you're aiming to enforce dependencies between layers, validate design rules, or maintain design integrity, NetArchTest provides a simple and effective mechanism to achieve these goals.

Why we gonna do?

Before we explore the intricacies of NetArchTest, it's crucial to understand the 'why' behind its adoption. NetArchTest project allows you create tests that enforce conventions for class design, naming and dependency in .Net code bases. These can be used with any unit test framework and incorporated into a build pipeline. It uses a fluid API that allows you to string together readable rules that can be used in test assertions.

There are plenty of static analysis tools that can evaluate application structure, but they are aimed more at enforcing generic best practice rather than application-specific conventions. The better tools in this space can be press-ganged into creating custom rules for a specific architecture, but the intention here is to incorporate rules into a test suite and create a self-testing architecture.

Software architecture, akin to a well-thought-out blueprint, is essential for a robust system. However, as deadlines loom and pressure mounts, corners might be cut, leading to architectural erosion. NetArchTest acts as a safeguard, ensuring that your architecture doesn't crumble under the weight of compromises. By automating the verification process, it guarantees that your software adheres to the predefined architectural patterns and rules, even amidst rapid development cycles.

The advantages of Architecture tests are as follows,

  • Maintain Design Integrity: NetArchTest acts as a guardian, preserving the consistent design and structure of your software as it evolves over time. It prevents architectural drift, ensuring that your initial vision remains intact.
  • Early Issue Detection: By catching architectural discrepancies early in the development process, NetArchTest prevents potential complications that might arise due to structural flaws. Early detection translates to easier debugging and resolution.
  • Improved Collaboration: Clear architectural rules and automated tests make it easier for developers to comprehend the system's design. This shared understanding fosters smoother collaboration, particularly in large teams or when onboarding new members.
  • Quality Assurance: A robust architecture is the cornerstone of high-quality software. NetArchTest contributes significantly to the overall quality and reliability of your software by validating structural decisions and enforcing design rules.

How we gonna do?

Implementing NetArchTest in your project is seamless. Start by creating a new test project within your development environment. Then, install the NetArchTest.Rules NuGet package, which equips you with the necessary boilerplate code to initiate your tests. The library's core revolves around the Types class, enabling you to load and filter types based on various criteria. Once your types are selected, you can define rules using Should or ShouldNot conditions, ensuring that your architecture aligns with your predefined specifications. Here are some of the conventions I prefer to implement in my projects.

Note: If you have not done so already, I recommend you read the article on Request Endpoint Response (REPR) pattern in ASP.NET WEB API.


using Microsoft.AspNetCore.Mvc;
using NetArchTest.Rules;
using System.Diagnostics.CodeAnalysis;
using Xunit.Abstractions;

namespace ILoveDotNet.FitnessTest;

public class ControllerTests(ITestOutputHelper output)
{
    [Fact]
    public void Should_Have_Only_One_Handler_Inside_Them()
    {
        var policy = Policy
                .Define("Controller Policies",
                    "Enforces all Endpoints to have only one handlers inside them.")
                .For(Types.InAssembly(typeof(Program).Assembly))
                .Add(types => types
                    .That()
		    .AreClasses()
		    .And()
		    .Inherit(typeof(ControllerBase))
		    .Should()
		    .MeetCustomRule(new OnlyOneHandleRule()),
                "Use One Handler Per Endpoint",
                "Controller endpoints should not have more than one handler.");

        policy.Evaluate().Report(output);
    }

    [Fact]
    public void Should_Have_Only_One_ActionResult_Inside_Them()
    {
        var policy = Policy
            .Define("Controller Policies",
                "Enforces all Endpoints to have only one Action Result inside them.")
            .For(Types.InAssembly(typeof(Program).Assembly))
            .Add(types => types
                    .That()
		    .AreClasses()
   		    .And()
		    .Inherit(typeof(ControllerBase))
		    .Should()
		    .MeetCustomRule(new OnlyOneActionResultRule()),
                "Use One Action Result Per Endpoint",
                "Controller endpoints should not have more than one Action Result.");
        
        policy.Evaluate().Report(output);
    }
}

public class OnlyOneHandleRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
        return type.Methods.Count(x => x.Name.Equals("HandleAsync")).Equals(1);
    }
}

public class OnlyOneActionResultRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
        return type.Methods.Count(x => x.IsPublic && !x.IsConstructor).Equals(1);
    }
}
            

using Application;
using Domain.Entity;
using Infrastructure;
using NetArchTest.Rules;
using Xunit.Abstractions;

namespace ILoveDotNet.FitnessTest;

public class CleanArchitectureTests(ITestOutputHelper output)
{
    [Fact]
    public void Domain_Should_Not_Have_Any_Dependencies()
    {
        var policy = Policy
            .Define("Clean Architecture Policies",
                "Enforces Domain Project should not have dependency on Infrastructure and Application Project.")
            .For(Types.InAssembly(typeof(BaseEntity).Assembly))
            .Add(types => types
                    .ShouldNot()
		    .HaveDependencyOnAny("Application", "Infrastructure"),
                "Remove Application and Infrastructure Project references from Domain Project",
                "Domain Project should not have dependency on Application Project and Infrastructure Project.");
        
        policy.Evaluate().Report(output);
    }

    [Fact]
    public void Application_Should_Not_Have_Dependency_On_Infrastructure()
    {
        var policy = Policy
            .Define("Clean Architecture Policies",
                "Enforces Application Project should not have dependency on Infrastructure Project.")
            .For(Types.InAssembly(typeof(ApplicationServiceRegistration).Assembly))
            .Add(types => types
                    .ShouldNot()
		    .HaveDependencyOn("Infrastructure"),
                "Remove Infrastructure Project reference from Application Project",
                "Application Project should not have dependency on Infrastructure Project");
        
        policy.Evaluate().Report(output);
    }

    [Fact]
    public void Infrastructure_Should_Depend_On_Domain()
    {
        var policy = Policy
            .Define("Clean Architecture Policies",
                "Enforces Infrastructure Project should have dependency on Domain Project.")
            .For(Types.InAssembly(typeof(InfrastructureServiceRegistration).Assembly))
            .Add(types => types
                    .That()
		    .HaveNameEndingWith("Repository")
		    .Should()
		    .HaveDependencyOn("Domain"),
                "Add Domain Project reference to Infrastructure Project",
                "Infrastructure Project should have dependency on Domain Project");
        
        policy.Evaluate().Report(output);
    }
}
            

using Application;
using MediatR;
using NetArchTest.Rules;
using Xunit.Abstractions;

namespace ILoveDotNet.FitnessTest;

public class CommandQueryTests(ITestOutputHelper output)
{
    [Fact]
    public void Should_Have_Request_To_Be_Sealed()
    {
        var policy = Policy
            .Define("Command Query Policies",
                "Enforces all Request to be sealed.")
            .For(Types.InAssembly(typeof(ApplicationServiceRegistration).Assembly))
            .Add(types => types
                    .That()
		    .AreClasses()
		    .And()
		    .ImplementInterface(typeof(IRequest<>))
		    .Should()
		    .BeSealed(),
                "Seal all Requests",
                "Command Query Request should be sealed.");
        
        policy.Evaluate().Report(output);
    }

    [Fact]
    public void Should_Have_Request_Handler_To_Be_Sealed()
    {
        var policy = Policy
            .Define("Command Query Policies",
                "Enforces all Request Handlers to be sealed.")
            .For(Types.InAssembly(typeof(ApplicationServiceRegistration).Assembly))
            .Add(types => types
                    .That()
		    .AreClasses()
		    .And()
		    .ImplementInterface(typeof(IRequestHandler<,>))
		    .Should()
		    .BeSealed(),
                "Seal all Request Handlers",
                "Command Query Request Handlers should be sealed.");
        
        policy.Evaluate().Report(output);
    }

    [Fact]
    public void Handlers_Must_End_With_Handler()
    {
        var policy = Policy
            .Define("Command Query Policies",
                "Enforces all Request Handlers to have proper naming convention.")
            .For(Types.InAssembly(typeof(ApplicationServiceRegistration).Assembly))
            .Add(types => types
                    .That()
		    .ImplementInterface(typeof(IRequestHandler<,>))
		    .Should()
		    .HaveNameEndingWith("Handler"),
                "All Request Handlers name should end with Handler",
                "Command Query Request Handlers name should end with Handler.");
        
        policy.Evaluate().Report(output);
    }
}
            

using Domain.Entity;
using NetArchTest.Rules;
using System.Diagnostics.CodeAnalysis;
using Xunit.Abstractions;

namespace ILoveDotNet.FitnessTest;

public class DomainTests(ITestOutputHelper output)
{
    [Fact]
    public void All_Entities_Should_Be_Encapsulated()
    {
    	var policy = Policy
           .Define("Domain Policies",
                "Enforces all members in Domain Entites to be encapsulated with private setters or init only setters.")
            .For(Types.InAssembly(typeof(BaseEntity).Assembly))
            .Add(types => types
                    .That()
                    .AreClasses()
		    .And()
		    .Inherit(typeof(BaseEntity))
		    .Should()
                    .MeetCustomRule(new EncapsulationRule()),
                "Use proper encapsulation",
                "Properties in Classes should only have private or init only setters.");
        
        policy.Evaluate().Report(output);
    }
}  

public class EncapsulationRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
        return TypeShouldNotHavePublicSetters(type) && TypeShouldNotHavePublicSetters(type.BaseType?.Resolve());
    }

    private static bool TypeShouldNotHavePublicSetters(TypeDefinition? type)
    {
        return type?.Properties
    		   .All(x => x.SetMethod is null // For IReadOnlyList
		   || !x.SetMethod.IsPublic // Allow Private and Protected
		   || x.SetMethod.ReturnType.FullName.Contains("IsExternalInit")) // For C# 9 init
		   ?? false;
    }
}
            

using FitnessTest.Rules;
using Domain.Entity;
using NetArchTest.Rules;
using System.Diagnostics.CodeAnalysis;
using Xunit.Abstractions;
using NetArchTest.Rules.Policies;
using FitnessTest.Utilities;

namespace FitnessTest;

[ExcludeFromCodeCoverage]
public class DomainTests(ITestOutputHelper output)
{
  [Fact]
  public void All_Entities_Should_Not_Have_Parameterless_Public_Constructor()
  {
    var policy = Policy
            .Define("Domain Policies",
                "Enforces all Entities should not have parameterless public constructor.")
            .For(Types.InAssembly(typeof(BaseEntity).Assembly))
            .Add(types => types
                    .That()
                    .AreClasses()
		    .And()
                    .Inherit(typeof(BaseEntity))
                    .Should()
                    .MeetCustomRule(new NoParameterlessPublicConstructorRule()),
                "Remove empty public constructor from all domain entites",
                "Domain Entity Classes should not have empty public constructors.");
        
    policy.Evaluate().Report(output);
  }
}

public class NoParameterlessPublicConstructorRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
    {
	var result = type.GetConstructors().Any(x => x.IsPublic && !x.HasParameters);		
	return !result;
    }
}
            

The above code snippets demonstrate how you can enforce architectural rules using NetArchTest. By defining rules that align with your architectural vision, you can ensure that your software remains consistent and resilient. Whether you're enforcing the REPR pattern in your endpoints, validating Clean Architecture principles, or enforcing encapsulation in domain entities, NetArchTest provides a robust mechanism to safeguard your architectural integrity.

But sometimes it will be difficult to debug these test to understand which class is making the violations. To identify them easily you can use the below policy extension to report and write failures to Test Output Logs.


using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using FluentAssertions.Execution;
using NetArchTest.Rules.Policies;
using Xunit.Abstractions;

namespace ILoveDotNet.FitnessTest;

[ExcludeFromCodeCoverage]
public static class PolicyExtensions
{
    public static void Report(this PolicyResults results, ITestOutputHelper output)
    {
        if (results.HasViolations)
        {
            output.WriteLine($"Policy violations found for: {results.Name}");

            foreach (var rule in results.Results)
            {
                if (rule.IsSuccessful)
                {
                    continue;
                }
                output.WriteLine("-----------------------------------------------------------");
                output.WriteLine($"Rule failed: {rule.Name}");

                foreach (var type in rule.FailingTypes)
                {
                    output.WriteLine($"\t {type.FullName}");
                }
            }

            output.WriteLine("-----------------------------------------------------------");
        }
        else
        {
            output.WriteLine($"No policy violations found for: {results.Name}");
        }
        
        using var scope = new AssertionScope();
        results.HasViolations.Should().BeFalse();
    }
}
            

Here are some design rules that you can enforce:

  • Domain entities must be encapsulated
  • Services must be internal
  • Entities and Value objects must be sealed
  • Controllers can't depend on repositories directly
  • Command (or query) handlers must follow a naming convention
  • Following naming conventions for classes

Summary

In essence, NetArchTest emerges as a proactive measure, ensuring that the backbone of your software remains resilient and consistent. By automating the verification of architectural patterns and design rules, it not only saves time but also prevents potential headaches in the long run. As software projects grow in complexity, embracing tools like NetArchTest becomes essential, safeguarding your architectural vision and bolstering the reliability of your software systems. So, the next time you embark on a development journey, consider NetArchTest as your loyal sentinel, guarding your software's architectural integrity with unwavering vigilance. >
👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Web API
  • Architecture Test
  • Fitness Functions
  • Fitness Test
  • NetArchTest