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
TIPUse
NetArchTest.eNhancedEditioninstead ofNetArchTest.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.
WARNINGThese tests don’t just check
usingstatements — 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));
}
TIPYou 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
NOTEArchitecture tests run very fast (typically < 1 second) because they only analyze metadata, without executing business logic.
Summary
| # | Test | Purpose |
|---|---|---|
| 1 | Layer Dependencies | Domain must not depend on outer layers |
| 2 | Controller Isolation | Controllers only call Application layer |
| 3 | Naming Conventions | Handlers, Validators follow naming rules |
| 4 | DDD Patterns | Entities, Value Objects, Domain Events follow rules |
| 5 | Circular Dependencies | No 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.
Subscribe to Newsletter
Get notified when I publish new posts. No spam, unsubscribe anytime.