883 words
4 minutes
5 Architecture Tests You Should Add to Your .NET Projects

You can have a perfect Clean Architecture on paper, but all it takes is one developer accidentally adding using Infrastructure in the Domain layer — and the whole architecture collapses without anyone noticing.

Architecture Tests solve this problem. They are unit tests that verify code structure instead of logic, ensuring architectural rules are enforced in your CI/CD pipeline.

In this post, I’ll introduce 5 practical architecture tests you should add to your .NET projects using NetArchTest.

Setup#

Use NetArchTest.eNhancedEdition — the actively maintained fork with bug fixes and the Slices API:

dotnet add package NetArchTest.eNhancedEdition
dotnet add package FluentAssertions
TIP

Use NetArchTest.eNhancedEdition instead of NetArchTest.Rules (unmaintained since 2021). The Enhanced Edition adds the Slices API for detecting circular dependencies.

Assembly Reference Markers#

Each project needs a marker class for easy assembly referencing in tests:

// Domain/AssemblyReference.cs
namespace Domain;

public static class AssemblyReference
{
    public static readonly System.Reflection.Assembly Assembly
        = typeof(AssemblyReference).Assembly;
}

Create the same for Application, Infrastructure, and WebApi.

Then in your test project:

using System.Reflection;
using NetArchTest.Rules;
using FluentAssertions;

public class ArchitectureTests
{
    private static readonly Assembly DomainAssembly
        = typeof(Domain.AssemblyReference).Assembly;
    private static readonly Assembly ApplicationAssembly
        = typeof(Application.AssemblyReference).Assembly;
    private static readonly Assembly InfrastructureAssembly
        = typeof(Infrastructure.AssemblyReference).Assembly;
    private static readonly Assembly PresentationAssembly
        = typeof(WebApi.AssemblyReference).Assembly;
}

1. Domain Layer Must Not Depend on Outer Layers#

This is the most critical rule in Clean Architecture: the Domain layer must be completely independent. It should not reference Application, Infrastructure, or Presentation.

[Fact]
public void Domain_Should_NotHaveDependencyOn_Application()
{
    var result = Types.InAssembly(DomainAssembly)
        .Should()
        .NotHaveDependencyOn(ApplicationAssembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Domain -> Application dependency found", result));
}

[Fact]
public void Domain_Should_NotHaveDependencyOn_Infrastructure()
{
    var result = Types.InAssembly(DomainAssembly)
        .Should()
        .NotHaveDependencyOn(InfrastructureAssembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Domain -> Infrastructure dependency found", result));
}

[Fact]
public void Domain_Should_NotHaveDependencyOn_Presentation()
{
    var result = Types.InAssembly(DomainAssembly)
        .Should()
        .NotHaveDependencyOn(PresentationAssembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Domain -> Presentation dependency found", result));
}

When a test fails, FailingTypes tells you exactly which class violated the rule — making it easy to fix.

WARNING

These tests don’t just check using statements — they also detect indirect dependencies through inheritance, generic type parameters, or method signatures.

2. Controllers Must Not Directly Access Repositories#

In Clean Architecture, Controllers should communicate through the Application layer (MediatR, services) instead of calling Infrastructure directly.

[Fact]
public void Controllers_Should_NotDependOn_Infrastructure()
{
    var result = Types.InAssembly(PresentationAssembly)
        .That()
        .HaveNameEndingWith("Controller")
        .ShouldNot()
        .HaveDependencyOn(InfrastructureAssembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Controllers bypass Application layer", result));
}

You can also be more specific — Controllers should not reference namespaces containing repositories:

[Fact]
public void Controllers_Should_NotDependOn_Repositories()
{
    var result = Types.InAssembly(PresentationAssembly)
        .That()
        .HaveNameEndingWith("Controller")
        .ShouldNot()
        .HaveDependencyOnAny(
            "Infrastructure.Repositories",
            "Infrastructure.Persistence")
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Controllers depend directly on repositories", result));
}

3. Handlers Must Follow Naming Conventions#

Consistent naming helps the team navigate the codebase. If you use CQRS, every handler should end with Handler, CommandHandler, or QueryHandler.

With MediatR#

[Fact]
public void MediatR_Handlers_Should_EndWith_Handler()
{
    var result = Types.InAssembly(ApplicationAssembly)
        .That()
        .ImplementInterface(typeof(MediatR.IRequestHandler<,>))
        .Should()
        .HaveNameEndingWith("Handler")
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "MediatR handlers with wrong naming", result));
}

With custom CQRS interfaces#

[Fact]
public void CommandHandlers_Should_EndWith_CommandHandler()
{
    var result = Types.InAssembly(ApplicationAssembly)
        .That()
        .ImplementInterface(typeof(ICommandHandler<>))
        .Should()
        .HaveNameEndingWith("CommandHandler")
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Command handlers with wrong naming", result));
}
TIP

You can apply the same approach to Validators (*Validator), Specifications (*Specification), or any convention your team agrees on.

4. Domain Entities Must Follow DDD Patterns#

Domain Events must be sealed#

Domain Events are immutable data — there’s no reason to inherit from them:

[Fact]
public void DomainEvents_Should_BeSealed()
{
    var result = Types.InAssembly(DomainAssembly)
        .That()
        .Inherit(typeof(DomainEvent))
        .Should()
        .BeSealed()
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Domain events must be sealed", result));
}

Value Objects must be sealed#

[Fact]
public void ValueObjects_Should_BeSealed()
{
    var result = Types.InAssembly(DomainAssembly)
        .That()
        .Inherit(typeof(ValueObject))
        .Should()
        .BeSealed()
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Value objects must be sealed", result));
}

Entities must not have public parameterless constructors#

EF Core needs a parameterless constructor, but it should be private to enforce domain invariants:

[Fact]
public void Entities_Should_NotHave_PublicParameterlessConstructor()
{
    var entityTypes = Types.InAssembly(DomainAssembly)
        .That()
        .Inherit(typeof(Entity))
        .GetTypes();

    var failingTypes = entityTypes
        .Where(t => t.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
                      .Any(c => c.GetParameters().Length == 0))
        .ToList();

    failingTypes.Should().BeEmpty(
        because: $"Entities should not expose public parameterless constructors. " +
                 $"Failing: {string.Join(", ", failingTypes.Select(t => t.Name))}");
}

5. No Circular Dependencies Between Layers#

This is the most powerful test — using the Slices API from the Enhanced Edition to detect circular dependencies:

[Fact]
public void Application_Should_NotDependOn_Presentation()
{
    var result = Types.InAssembly(ApplicationAssembly)
        .Should()
        .NotHaveDependencyOn(PresentationAssembly.GetName().Name)
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: FormatFailingTypes(
            "Application -> Presentation dependency found", result));
}

Detect Circular Dependencies with Slices API#

[Fact]
public void FeatureModules_Should_NotHave_CrossDependencies()
{
    var result = Types.InAssembly(ApplicationAssembly)
        .Slice()
        .ByNamespacePrefix("MyApp.Application.Features")
        .Should()
        .NotHaveDependenciesBetweenSlices()
        .GetResult();

    result.IsSuccessful.Should().BeTrue(
        because: "Feature modules should not depend on each other");
}

This test is especially useful for Modular Monoliths — each feature module must be independent, communicating through contracts/events instead of direct references.

Helper Method#

A small helper for clear failure messages:

private static string FormatFailingTypes(
    string message, TestResult result)
{
    if (result.IsSuccessful) return message;

    var failingNames = result.FailingTypes?
        .Select(t => t.FullName)
        .Take(10) ?? [];

    return $"{message}: [{string.Join(", ", failingNames)}]";
}

When to Run Architecture Tests?#

  • Local: Run alongside unit tests during development
  • CI/CD: Run in the pipeline — fail the build on violations
  • PR Review: Architecture tests replace the need for reviewers to manually check dependencies
NOTE

Architecture tests run very fast (typically < 1 second) because they only analyze metadata, without executing business logic.

Summary#

#TestPurpose
1Layer DependenciesDomain must not depend on outer layers
2Controller IsolationControllers only call Application layer
3Naming ConventionsHandlers, Validators follow naming rules
4DDD PatternsEntities, Value Objects, Domain Events follow rules
5Circular DependenciesNo circular dependencies between modules

Architecture tests help you “shift left” — catching architectural violations at code time, rather than waiting for code review or worse, production.

With just one test project and a few dozen lines of code, you can protect your architecture 24/7.

NeVeSpl
/
NetArchTest.eNhancedEdition
Waiting for api.github.com...
00K
0K
0K
Waiting...
5 Architecture Tests You Should Add to Your .NET Projects
https://www.devwithxuan.com/en/posts/architecture-tests-dotnet/en/
Author
XuanPD
Published at
2026-03-09
Share:

Subscribe to Newsletter

Get notified when I publish new posts. No spam, unsubscribe anytime.

Comments