Feature Comparison with NetArchTest and ArchUnitNET
NetArchTest and ArchUnitNET are the two established open-source libraries for architecture tests in .NET. aweXpect.Reflection covers the same ground (dependency rules, naming conventions, cycle detection) as part of a general reflection assertion library. This page maps the three approaches against each other.
The same rule in all three libraries
"The presentation layer must not reference the data layer":
- aweXpect.Reflection
- NetArchTest
- ArchUnitNET
await Expect.That(Types.InNamespace("MyApp.Presentation"))
.DoNotDependOn("MyApp.Data")
.Because("the presentation layer is decoupled from persistence");
No separate loader or result handling: the selection scans the loaded assemblies, and a violation fails the test with a message listing the offending types.
var result = Types.InAssembly(typeof(MyService).Assembly)
.That().ResideInNamespace("MyApp.Presentation")
.ShouldNot().HaveDependencyOn("MyApp.Data")
.GetResult();
Assert.True(result.IsSuccessful); // violations are in result.FailingTypeNames
GetResult() returns a TestResult with IsSuccessful and the failing types; turning that into a useful
test failure (assertion + message) is left to you.
private static readonly Architecture Architecture =
new ArchLoader().LoadAssemblies(typeof(MyService).Assembly).Build();
IArchRule rule = Types().That().ResideInNamespace("MyApp.Presentation")
.Should().NotDependOnAny(Types().That().ResideInNamespace("MyApp.Data"))
.Because("the presentation layer is decoupled from persistence");
rule.Check(Architecture); // throws with the violations listed
The architecture is loaded explicitly (and cached statically); Check comes from a test-framework
integration package and throws a FailedArchRuleException describing each violation.
Conceptual model
- NetArchTest (
NetArchTest.Rules) is a deliberately small, type-level rule engine:Types.InAssembly(…)→.That()predicates →.Should()/.ShouldNot()conditions →.GetResult(). It reads assemblies with Mono.Cecil and evaluates everything againstTypeDefinitions. There are no member-level rules and no assertion integration: you checkIsSuccessfulyourself. - ArchUnitNET (
TngTech.ArchUnitNET) is the .NET port of Java's ArchUnit. It loads assemblies into its ownArchitecturemodel (also via Mono.Cecil), on which rules fromArchRuleDefinition(Types(),Classes(),MethodMembers(), …) are checked through per-framework integration packages (xUnit, NUnit, MSTest, TUnit). It is the most feature-rich of the three for pure architecture testing: slices, cycle rules and PlantUML import/export. - aweXpect.Reflection works directly on
System.Reflectionobjects inside the aweXpect assertion model: a "layer" is a reusableFiltered.Typesselection, an architecture rule is an ordinaryawait Expect.That(…)expectation, and the same vocabulary extends to methods, properties, fields, events, constructors and assemblies for convention tests.
Coverage overview
Legend: ✅ built-in · ⚠️ partial or via a general mechanism · ❌ not available.
| Capability | NetArchTest | ArchUnitNET | aweXpect.Reflection |
|---|---|---|---|
| Rule subjects | types only | types + members | assemblies, types + all member kinds |
| Selection by assembly / namespace | ✅ | ✅ | ✅ |
| Naming rules (prefix/suffix/regex) | ✅ | ✅ | ✅ (plus wildcard, custom comparer) |
| Type kinds | ⚠️ classes, interfaces | ✅ classes, interfaces, enums, structs, records | ✅ plus delegates, exceptions, attributes, ref structs, record structs |
| Access modifiers | ⚠️ public, nested | ✅ | ✅ |
| Attribute rules | ✅ presence | ✅ incl. argument values | ✅ incl. typed predicate on the attribute instance |
| Member-level rules (methods, properties, fields) | ❌ | ✅ | ✅ plus events, constructors, parameter modifiers, async, extension methods, operators, required / nullable members |
| Immutability / nullability rules | ✅ | ⚠️ immutability | ✅ type- and member-level |
| Dependency rules (on / not on / only on) | ✅ | ✅ | ✅ |
| Body-level (IL) dependency detection | ✅ | ✅ | ⚠️ signature-level default, IL via custom resolver |
| Dependency cycle detection | ❌ | ✅ via slices | ✅ via namespaces / slice roots |
| Failure messages explain the violation | ❌ failing types only | ✅ | ✅ all violations, grouped per rule |
| Combine rules in one verification | ✅ policies | ✅ And/Or | ✅ Expect.ThatAll |
| Exemptions to a rule | ⚠️ custom rule | ⚠️ predicates | ✅ Except(…) / Except<T>() |
| Test-framework integration | ❌ assert manually | ✅ xUnit / NUnit / MSTest / TUnit packages | ✅ framework-agnostic (aweXpect core) |
| PlantUML diagrams (import/export) | ❌ | ✅ | ❌ |
Dependency detection depth
The most important technical difference is where dependencies are read from:
NetArchTest and ArchUnitNET parse assemblies with Mono.Cecil and walk method bodies: an
instantiation (new Foo()), a static call or a cast inside a method counts as a dependency.
aweXpect.Reflection's default resolver works on reflection metadata and sees only the declared
signatures (base type, interfaces, member/parameter/return types, attributes; see
type dependencies). Body-only references are invisible to
it unless you plug in an IL-level
custom dependency resolver, e.g. backed by Mono.Cecil.
In return, the signature-level default needs no assembly file on disk, never diverges from the runtime types, and filters out compiler plumbing (state machines, synthesized records members, nullability attributes, …) so rules only see code you actually wrote.
Cycle detection
- aweXpect.Reflection
- NetArchTest
- ArchUnitNET
// Namespaces under MyApp must be free of (transitive) cycles,
// grouped into one slice per direct sub-namespace of MyApp
await Expect.That(Types.InNamespace("MyApp"))
.HaveNoDependencyCycles("MyApp");
Cycles are reported as namespace chains (e.g. MyApp.Orders -> MyApp.Billing -> MyApp.Orders); see
dependency cycles for the namespace-family and slice-root
semantics.
// Not supported: NetArchTest has no cycle detection or slice concept.
SliceRuleDefinition.Slices()
.Matching("MyApp.(*)")
.Should().BeFreeOfCycles()
.Check(Architecture);
Slices are matched with (*)/(**) namespace patterns; cycle output lists the slice chain and the
type-to-type references behind each edge.
Failure messages
What failure of the rule from the top of this page looks like in
each library, when OrderService in the presentation layer references OrderRepository in the data layer:
- aweXpect.Reflection
- NetArchTest
- ArchUnitNET
Expected that types within namespace "MyApp.Presentation" in all loaded assemblies
all do not depend on namespace "MyApp.Data", because the presentation layer is decoupled from persistence,
but it contained types with the dependency [
OrderService
]
The selection and the rule describe themselves in the message, including the Because(…) reason,
followed by the violating types.
Assert.True() Failure
Expected: True
Actual: False
Neither the rule nor the violators are part of the message: Assert.True(result.IsSuccessful) reports
only the boolean (and a custom user message replaces even that). A useful failure message has to be built
by hand from result.FailingTypeNames.
"Types that reside in namespace with full name "MyApp.Presentation" should not depend on any Types that reside in namespace with full name "MyApp.Data" because the presentation layer is decoupled from persistence" failed:
MyApp.Presentation.OrderService does depend on "MyApp.Data.OrderRepository"
The FailedArchRuleException message quotes the rule, including the Because(…) reason, and lists each
violating object with a failure description.
Several aweXpect rules combine into one verification with Expect.ThatAll(…), which numbers each rule
and reports all failures together; see
layers as type selections for a full example
with output.
Beyond architecture rules
NetArchTest stops at types, and ArchUnitNET's member rules cover names, visibility and a few modifiers. Convention tests over member details are where aweXpect.Reflection's filter and assertion vocabulary goes further; the same fluent style covers e.g.:
// All async methods end in "Async"
await Expect.That(In.AssemblyContaining<MyClass>()
.Methods().WhichAreAsync())
.HaveName("Async").AsSuffix();
// Every public method with an [HttpGet] or [HttpPost] attribute accepts a CancellationToken
await Expect.That(In.AssemblyContaining<MyController>()
.Public.Methods().With<HttpGetAttribute>().OrWith<HttpPostAttribute>())
.HaveParameter<CancellationToken>();
// Serializable types have exactly one parameterless constructor
await Expect.That(In.AllLoadedAssemblies()
.Types().With<SerializableAttribute>())
.ContainConstructors(c => c.WithoutParameters()).Exactly(1.Times());