dateo. Coding Blog

Coding, Tech and Developers Blog

.NET
architecture

Enforce architectural constrains using NetArchTest

Dennis Frühauff on May 7th, 2024

Introduction

Testing, reviewing, and enforcing architectural constraints and conventions in solutions can be a big challenge even in smaller software development teams. In any properly maintained code base that I've worked in, some rules and conventions were being employed by the development team. Some of these rules were obvious, some were implicitly agreed on, and some were explicitly enforced, e.g., by pipeline checks.


Making sure that architectural principles are followed can be quite a challenge. Especially in teams with fluctuating team members, or in very big software teams, the code base can tend to slowly disintegrate into an unmaintainable mess. So making sure that basic principles are followed so that future-us can still effectively maintain the application can be an important part of your work.


Different tools are commonly used to make sure that conventions are being followed in a code base:


Documentation

We all love documenting code. Whether we use separate Markdown files, code comments, a team wiki or any other solution, I safely assume that we all document at least some architectural decisions somewhere.


  • "No 3rd party references in the Core project."
  • "The DbContext must only be referenced from a repository."
    are only some examples that I've seen.
    Needless to say, documenting decisions is useful, especially for later reference. It might not be helpful for very small conventions, but at least large-scale architectural design decisions should be documented somewhere. And, also, everyone should be made aware of them at some point.

Code reviews

Teams working in an environment that utilizes code reviews are likely to pay attention to architectural changes or mistakes. In the best case, more experienced team members will spot changes that violate their team's standards and can request to change that. Ideally, they can backup their remarks with explanations and documented decisions. This also helps spread the information across the team which is especially important for newer team members.


3rd party tooling, build pipeline checks

There are a few solutions out there to help enforce architectural rules in your codebase. The most famous example is probably NDepend, which I have covered in a past article. It is extremely powerful and can be integrated into your CI/CD pipelines to help monitor and enforce dedicated architectural rule sets in your code. But it also has to be paid for and maintained, so it is probably not the most lightweight approach to go.


Testing your architecture with NetArchTest

Another option that you can very easily implement is unit or integration tests that check whether certain principles are followed in your code base or not. They are easy to set up, fast, provide instant feedback to every developer and can also be regarded as some kind of documentation of your conventions.


While it wouldn't be too hard to implement a set of rules using reflection yourself, there is actually a NuGet package doing exactly that for you: NetArchTest.


Let's see how we can implement a set of rules in a unit test. You can find all the code related to this article on GitHub.In that solution, we have a Core project, that we assume to hold all the business logic and a project Infrastructure that is supposed to contain external dependencies. Both of these are almost empty, they are just for demonstration purposes. The solution also contains a unit test project where the architectural constraints are tested.


So let's say that our team agreed to make all classes in the Infrastructure project internal, i.e., exposing them to Core only via interfaces. We can define a policy for this single rule like this:


var policy = Policy.Define("Infrastructure policies", "Enforces different checks on infrastructural types")
    .For(Types.InAssembly(typeof(Infrastructure.Infrastructure).Assembly))
    .Add(types => types
            .That().AreClasses()
            .Should().NotBePublic(),
        "Use correct modifiers",
        "Classes in Infrastructure should not be public"
        );

Now, this policy alone does not verify anything. In order to evaluate it, you need to call Evaluate(). After that, a property HasViolations will tell you whether this policy has seen any violation of your rules.


A policy can encompass any number of rules. So let's say that we want to make sure that there is no reference to Newtonsoft.Json in any of our classes in the same policy:


var policy = Policy.Define("Infrastructure policies", "Enforces different checks on infrastructural types")
    .For(Types.InAssembly(typeof(Infrastructure.Infrastructure).Assembly))
    .Add(types => types
            .That().AreClasses()
            .Should().NotBePublic(),
        "Use correct modifiers",
        "Classes in Infrastructure should not be public"
        )
    .Add(types => types
            .That().AreClasses()
            .Should().NotHaveDependencyOn("Newtonsoft.Json"),
        "Use only System.Text.Json",
    "Classes in Infrastructure should not use Newtonsoft");

In order to be useful to someone, this policy needs to be evaluated inside a unit test, which should then fail for any violations and report the results to the developer executing it. Let's have an extension for this:


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)
                {
                    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();
    }
}

Finally, the full test will look something like this:


public class ArchitectureTests
{
    private readonly ITestOutputHelper output;

    public ArchitectureTests(ITestOutputHelper output)
    {
        this.output = output;
    }
    
    [Fact]
    public void ArchitecturalRules_InInfrastructure_AreMet()
    {
        var policy = Policy.Define("Infrastructure policies", "Enforces different checks on infrastructural types")
            .For(Types.InAssembly(typeof(Infrastructure.Infrastructure).Assembly))
            .Add(types => types
                    .That().AreClasses()
                    .Should().NotBePublic(),
                "Use correct modifiers",
                "Classes in Infrastructure should not be public"
            )
            .Add(types => types
                    .That().AreClasses()
                    .Should().NotHaveDependencyOn("Newtonsoft.Json"),
                "Use only System.Text.Json",
                "Classes in Infrastructure should not use Newtonsoft");

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

Rules already provided by NetArchTest

Using the above approach, assume you can already represent many of the architectural conventions or rules your team has agreed upon. If you are interested in the different existing rules that are provided by the NuGet package, please take a look here.


There is also the possibility to implement custom rules by implementing the interface ICustomRule and verifying it in your policies:


public class CustomRule : ICustomRule
    {
        public bool MeetsRule(TypeDefinition type)
        {
            return true;
        }
    }

This will give you a great deal of flexibility to test whether architectural guidelines are met in your codebase.


Summary

For those of you who are interested in actually enforcing certain architectural standards throughout your codebase, leveraging unit tests for that could be a great solution. They are easy to implement, lightweight, free, and provide instant feedback to anyone making changes to your application. In that sense, they can also serve as a means of documentation for your team.
NetArchTest is an example of a NuGet package providing a fluent API to verify your own rules in your code. Feel free to check it out yourself and take a look at the sample solution for this article on GitHub.



Please share on social media, stay in touch via the contact form, and subscribe to our post newsletter!

Be the first to know when a new post was released

We don’t spam!
Read our Privacy Policy for more info.

We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.