How to enforce architecture rules in C#

Unit testing is how we ensure that the blocks of code we write do what we intended them to do. There are some open-source frameworks available to unit test .NET applications, namely, NUnit and xUnit.Net. You should always incorporate unit testing in your software development workflow to reduce or eliminate errors in your applications.

You might also take advantage of frameworks such as ArchUnit or NetArchTest to write unit tests that can help enforce architectural rules. Inspired by ArchUnit for Java, Ben Morris’s NetArchTest is a simple framework that can be used to enforce architecture rules in .NET Framework or .NET Core as well as in .NET 6 projects.

This article talks about the importance of enforcing architectural rules and how to leverage NetArchTest to achieve this. To work with the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.

The need for enforcing architectural rules

There are plenty of static code analysis frameworks and tools available for checking code quality in .NET, .NET Core, or .NET 6. Two popular tools are SonarQube and NDepend, for starters. Static code analysis is also available as part of Visual Studio.

However, a few of these tools help you preserve the architecture design patterns or enforce architecture rules in your source code. And if you don’t regularly validate or enforce these rules, the design or architecture of your application will degrade over time. Eventually you will discover that maintaining the codebase has become a daunting task.

While the static code analysis tools help you to validate or enforce generic best practices, you can take advantage of NArchTest to create unit tests that enforce the architecture rules in your .NET, .NET Core, and .NET 6 applications. These include conventions for class design, naming, and dependency in your codebases.

You can use NArchTest in your unit test methods and then incorporate these test methods in the build and release pipeline so that the architecture rules are validated automatically with each check-in.

Create a Unit Test project in Visual Studio 2022

First off, let’s create Unit Test project in Visual Studio 2022 using the xUnit Test Project template. Following these steps will create a new Unit Test project in Visual Studio 2022:

  1. Launch the Visual Studio 2022 IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “xUnit Test Project” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences.
  7. Click Next.
  8. In the “Additional Information” window shown next, select .NET 6.0 as the target framework from the drop-down list at the top. Leave the “Authentication Type” as “None” (default).
  9. Ensure that the check boxes “Enable Docker,” “Configure for HTTPS,” and “Enable Open API Support” are unchecked as we won’t be using any of those features here.
  10. Click Create.

This will create a new xUnit project in Visual Studio 2022. We’ll use this project in the subsequent sections of this article.

Create a Class Library project in Visual Studio 2022

Let’s now create a class library project in Visual Studio 2022. Following these steps will create a new class library project in Visual Studio 2022:

  1. Launch the Visual Studio 2022 IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Class Library” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Click Next.
  7. In the “Additional Information” window shown next, select .NET 6.0 as the target framework from the drop-down list at the top.
  8. Click Create.

This will create a new Class Library project in Visual Studio 2022. We’ll use this project in the subsequent sections of this article.

Create model classes in .NET 6

Let’s assume that the name of the Class Library project is Core.Infrastructure. In the Solution Explorer window, choose this project and then click “Add -> New Folder” to add a new solution folder to the project. Models should have the same name as their solution folder.

Now create a class named BaseModel inside the Models solution folder and insert the following code:

public abstract class BaseModel
    {
        public int Id { get; set; }
    }

Create two more model classes named Product and Customer. Each of these two classes should extend the BaseModel class as shown below.

public class Product: BaseModel
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
public class Customer: BaseModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Create service classes in .NET 6

Create another solution folder in the same project and name it Services. Create an interface named IBaseService inside this solution folder and give it the following code:

public interface IBaseService
{
    public void Initialize();
}

The Initialize method must be implemented by all classes that implement this interface. The ProductService and CustomerService classes implement the IBaseService interface as shown in the code snippet given below.

//ProductService.cs
using Core.Infrastructure.Models;
namespace Core.Infrastructure.Services
{
    public sealed class ProductService: IBaseService
    {
        public void Initialize()
        {
            //Write your implementation here
        }
        public List<Product> GetProducts()
        {
            return new List<Product>();
        }
    }
}

//CustomerService.cs
using Core.Infrastructure.Models;
namespace Core.Infrastructure.Services
{
    public sealed class CustomerService: IBaseService
    {
        public void Initialize()
        {
            //Write your implementation here
        }
        public List<Customer> GetCustomers()
        {
            return new List<Customer>();
        }
    }
}

Note that, for the purposes of this simple implementation, the Initialize method of both the ProductService class and the CustomerService class has been left blank. You can write your own implementation for these.

Install the NetArchTest.Rules NuGet package

So far so good. Now add the NetArchTest.Rules NuGet package to your project. To do this, select the project in the Solution Explorer window and right-click and select “Manage NuGet Packages.” In the NuGet Package Manager window, search for the NetArchTest.Rules package and install it.

Alternatively, you can install the package via the NuGet Package Manager console by entering the line shown below.

PM> Install-Package NetArchTest.Rules

Write architecture unit tests in .NET 6

Lastly, you should write the architecture unit tests to check if the source code under test conforms to your standards. Note that the term “standards” here is relative, and you may assume that these standards will be defined by you.

The following test method verifies that your service classes have a name with a Service suffix.

[Fact]
public void ServiceClassesShouldHaveNameEndingWithService()
{
    var result = Types.InCurrentDomain()
                 .That().ResideInNamespace(("Core.Infrastructure.Services"))
                 .And().AreClasses()
                 .Should().HaveNameEndingWith("Service")
                 .GetResult();
    Assert.True(result.IsSuccessful);
}

You could have another rule that verifies that all of your service classes implement the IBaseService interface. The following test method illustrates how this can be achieved.

[Fact]
public void ServiceClassesShouldImplementIBaseServiceInterface()
{
   var result = Types.InCurrentDomain()
                .That().ResideInNamespace(("Core.Infrastructure.Services"))
                .And().AreClasses()
                .Should().ImplementInterface(typeof(IBaseService))
                .GetResult();
   Assert.True(result.IsSuccessful);
}

You could also have a rule that verifies that the service classes are public and not sealed. If these classes are sealed, you won’t be able to extend them further.

[Fact]
public void ServiceClassesShouldBePublicAndNotSealed ()
{
    var result = Types.InCurrentDomain()
                .That().ResideInNamespace(("Core.Infrastructure.Services"))
                .Should().BePublic().And().NotBeSealed()
                .GetResult();
    Assert.True(result.IsSuccessful);
}

When you run these test methods, you should find that all of them pass, ie, they will be successful. Try changing the code and re-running the tests to check conformance to the rules we discussed.

netarchtest example IDG

The NetArchTest unit tests in action.

Remember that in the newer versions of C# you can have a default implementation of members in an interface. So, if you have an interface that is implemented by one or more classes, you can write the default implementation in the interface. This holds true if you’re writing code that is common across all implementations of the interface.

Copyright © 2022 IDG Communications, Inc.

Leave a Comment