Skip to main content

Command Palette

Search for a command to run...

Enforcing Architecture in .NET

Published
9 min read
Enforcing Architecture in .NET

Most software systems do not fail because developers cannot write code. They fail because you cannot control how that code evolves over time. Architecture starts clear, clean, and well-structured. Six months later, boundaries blur. Dependencies creep inward. Domain logic leaks into infrastructure. Services begin calling each other directly. The carefully designed structure becomes a tangled web.

This happens because architecture is usually treated as documentation rather than enforcement. You define layering rules, module boundaries, and dependency directions, but those rules exist only as diagrams and wiki pages. They rely on discipline, code reviews, and team knowledge to maintain consistency. As you grow and deadlines tighten, these human safeguards become insufficient.

This is where architectural testing changes everything.

NetArchTest is a lightweight yet powerful library that allows you to express architectural rules as executable tests. Instead of hoping developers respect boundaries, you verify them automatically. Instead of discovering violations during late reviews, you detect them immediately during CI builds. Instead of architecture drifting silently, you make it impossible for violations to enter the codebase unnoticed.

I briefly covered using this tool back in my Modular Monolith walkthrough but decided it deserves its own post so this article explores how NetArchTest works, how to integrate it into a real .NET solution, and how to use it to enforce architectural discipline in modular monoliths, layered systems, and large enterprise applications.

The Problem with Traditional Architectural Governance

Before understanding NetArchTest, it is important to understand why architectural governance often fails.

You define architecture using documentation. Draw diagrams showing for example, layers such as Presentation, Application, Domain, and Infrastructure. They specify that dependencies must flow inward toward the domain. They state that certain modules must not reference each other.

The problem is that documentation does not enforce anything.

Developers under time pressure might introduce shortcuts. A controller may reference a repository directly. An infrastructure class might begin calling domain logic. A shared project might gradually accumulate unrelated responsibilities. These violations often seem small at first, but they compound over time.

Code reviews attempt to catch these issues, but reviewers focus primarily on correctness and functionality. Architectural violations are subtle and easily missed. Furthermore, as systems grow, no single reviewer understands the entire architecture well enough to detect every violation.

Static analysis tools provide some assistance, but they typically focus on style, complexity, or language-level issues. They do not understand business-level architectural rules such as module boundaries or dependency directions.

NetArchTest fills this gap by allowing you to define architecture rules directly in code.

What NetArchTest Actually Does

NetArchTest enables developers to inspect compiled assemblies and verify structural properties. It operates at the level of types, namespaces, dependencies, and inheritance relationships.

Instead of testing behaviour, it tests structure.

You can ask questions such as:

Does any class in the Domain layer reference Infrastructure?

Are all service classes named correctly?

Are certain types sealed or abstract?

Do controllers only depend on application layer interfaces?

Are modules independent from each other?

NetArchTest answers these questions by analysing metadata from compiled assemblies. Because it runs against compiled code, it is extremely fast and integrates seamlessly into standard unit test pipelines.

This approach transforms architecture from a set of conventions into a set of enforceable guarantees.

Installing NetArchTest

Adding NetArchTest to a solution is straightforward. It is typically installed in a dedicated test project responsible for architectural verification.

You add the package via NuGet:

dotnet add package NetArchTest.Rules

Most Developers create a test project named something like:

ArchitectureTests

This project references the assemblies you want to analyse. It does not contain business logic tests. Its sole responsibility is enforcing architectural rules.

This separation keeps architecture validation distinct from functional testing.

Understanding the Core Concepts

NetArchTest revolves around a few core abstractions.

The first is Types. This represents a collection of types extracted from an assembly. You start every test by selecting which assembly or namespace you want to analyse.

The second is Rules. These define conditions that types must satisfy. Rules can check naming conventions, inheritance relationships, dependency restrictions, and more.

The third is Conditions. Conditions specify whether rules must pass or fail, such as ensuring a type should or should not have certain dependencies.

Finally, there are Results. After applying rules, NetArchTest returns a result indicating whether the architecture rule passed and which types violated it.

This model makes architectural testing expressive and readable.

A Simple Example

Take a standard layered architecture with separate projects for Domain, Application, and Infrastructure.

A fundamental rule is that the Domain layer must not depend on Infrastructure.

You can express this rule in NetArchTest as follows:

using NetArchTest.Rules;

public class DomainDependencyTests
{
    [Fact]
    public void Domain_Should_Not_Depend_On_Infrastructure()
    {
        var result = Types
            .InAssembly(typeof(DomainAssemblyMarker).Assembly)
            .ShouldNot()
            .HaveDependencyOn("MyApp.Infrastructure")
            .GetResult();

        Assert.True(result.IsSuccessful);
    }
}

This test inspects all types in the Domain assembly and ensures none reference the Infrastructure namespace.

If a developer accidentally introduces a dependency, this test fails immediately during CI.

Architecture is now enforced automatically.

Enforcing Naming Conventions

Naming conventions are often overlooked, but they play a crucial role in maintainability. NetArchTest can enforce these rules consistently.

Suppose all command handlers must end with the suffix "Handler".

You can enforce this with:

var result = Types
    .InNamespace("MyApp.Application.Commands")
    .Should()
    .HaveNameEndingWith("Handler")
    .GetResult();

This ensures consistent naming across the application layer.

Such rules prevent gradual erosion of structure as new developers join the project.

Enforcing Layered Dependency Direction

One of the most powerful uses of NetArchTest is enforcing dependency direction in layered architectures.

For example, in Clean Architecture, dependencies must flow inward toward the Domain layer. Infrastructure can depend on Application and Domain, but not the other way around.

You can enforce this rule like this:

var result = Types
    .InAssembly(typeof(ApplicationAssemblyMarker).Assembly)
    .ShouldNot()
    .HaveDependencyOn("MyApp.Infrastructure")
    .GetResult();

This prevents infrastructure concerns from leaking into application logic.

Over time, this rule preserves architectural purity.

Enforcing Module Boundaries in Modular Monoliths

For modular monoliths, architectural enforcement becomes even more critical.

Each module should be independent. Modules should communicate through well-defined interfaces rather than direct references.

NetArchTest allows you to enforce module isolation.

For example, suppose you have modules named Users and Billing.

You can ensure the Users module does not directly depend on Billing:

var result = Types
    .InNamespace("MyApp.Modules.Users")
    .ShouldNot()
    .HaveDependencyOn("MyApp.Modules.Billing")
    .GetResult();

This protects module boundaries and prevents tight coupling.

Without such enforcement, modular monoliths often degrade into distributed spaghetti.

Testing for Layer Violations via Inheritance

Another important use case is ensuring that certain classes inherit from specific base types.

For example, all domain entities might be required to inherit from a base Entity class.

You can enforce this rule:

var result = Types
    .InNamespace("MyApp.Domain.Entities")
    .Should()
    .Inherit(typeof(Entity))
    .GetResult();

This ensures consistency in domain modelling practices.

Enforcing Interface Usage

A common architectural rule is that higher layers should depend only on interfaces, not concrete implementations.

For example, controllers should depend only on application interfaces.

NetArchTest can verify this:

var result = Types
    .InNamespace("MyApp.Api.Controllers")
    .ShouldNot()
    .HaveDependencyOn("MyApp.Infrastructure")
    .GetResult();

This guarantees proper abstraction boundaries.

Organising Architecture Tests

As systems grow, architecture tests should be organised clearly.

Most teams structure them by architectural concern.

You might have test classes such as:

LayerDependencyTests

NamingConventionTests

ModuleIsolationTests

DomainIntegrityTests

This organisation makes the intent of each rule clear and ensures the test suite remains maintainable.

Integrating NetArchTest into CI/CD

Architecture tests should run automatically during CI builds. NetArchTest, being lightweight, adds minimal overhead to build times. When a rule is violated, the build fails immediately, providing developers with clear feedback on which types caused the failure. This creates a strong feedback loop that prevents architectural drift. Over time, this automated enforcement becomes one of the most valuable safeguards in a mature codebase.

Combining NetArchTest with Other Tools

These tests work best when combined with other architectural enforcement strategies. You can pair them with Roslyn analysers for compile time checks, solution reference restrictions to prevent project dependencies, code review guidelines for contextual validation, and documentation to explain architectural intent. Together, these tools create a multi-layered defence against architectural decay.

Practical Tips for Real World Use

When introducing NetArchTest into an existing system, start small by enforcing only the most critical rules, such as preventing domain dependencies on infrastructure. Gradually expand coverage to include module boundaries, naming conventions, and inheritance requirements. Avoid creating overly rigid rules that hinder development flexibility, architecture enforcement should support productivity rather than obstruct it. Keep rules readable and well-documented so future developers understand their purpose.

Why Architectural Testing Changes Team Behaviour

Combining NetArchTest with other architectural enforcement strategies, such as Roslyn analysers for compile time checks, solution reference restrictions to prevent project dependencies, code review guidelines for contextual validation, and documentation to explain architectural intent, creates a multi-layered defence against architectural decay. When introducing NetArchTest into an existing system, start small by enforcing only the most critical rules, like preventing domain dependencies on infrastructure, and gradually expand to include module boundaries, naming conventions, and inheritance requirements. Avoid overly rigid rules that hinder development flexibility, architecture enforcement should support productivity rather than obstruct it. Keep rules readable and well-documented for future developers. When developers know architectural rules are enforced automatically, they design solutions more carefully and avoid shortcuts, as violations will be detected immediately. Architecture becomes a living, enforceable contract rather than an aspirational guideline, significantly improving long-term system maintainability.

The Long Term Impact

Over months and years, architectural testing prevents the slow decay that affects most large systems.

Boundaries remain intact. Dependencies remain predictable. Modules stay independent.

Teams can refactor with confidence because structural guarantees are always validated.

NetArchTest transforms architecture from a fragile concept into a durable, enforceable reality.

Architecture is easy to design but difficult to maintain. Without enforcement, even the best designs gradually erode under the pressure of deadlines and evolving requirements.

This provides a practical solution by allowing developers to encode architectural rules as executable tests. These tests run automatically, detect violations instantly, and prevent structural decay.

By integrating NetArchTest into your .NET solution, you transform architecture from documentation into enforcement. You create a system where boundaries remain intact, dependencies remain controlled, and long-term maintainability becomes achievable.

For teams building large enterprise systems, modular monoliths, or long lived platforms, this capability is essential.